mirror of
https://github.com/spalencsar/bearhub.git
synced 2026-07-09 00:34:16 +02:00
feature: custom software suggestions
This commit is contained in:
@@ -22,12 +22,9 @@ URL_ENVIRONMENT_SETTINGS = f'https://raw.githubusercontent.com/vinifmor/bauh-fil
|
||||
DESKTOP_ENTRY_PATH_PATTERN = f'{DESKTOP_ENTRIES_DIR}/{__app_name__}.web.' + '{name}.desktop'
|
||||
URL_FIX_PATTERN = "https://raw.githubusercontent.com/vinifmor/bauh-files/master/web/env/v2/fix/{domain}/{electron_branch}/fix.js"
|
||||
URL_PROPS_PATTERN = "https://raw.githubusercontent.com/vinifmor/bauh-files/master/web/env/v2/fix/{domain}/{electron_branch}/properties"
|
||||
URL_SUGGESTIONS = "https://raw.githubusercontent.com/vinifmor/bauh-files/master/web/env/v2/suggestions.yml"
|
||||
UA_CHROME = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36'
|
||||
TEMP_PATH = f'{TEMP_DIR}/web'
|
||||
SEARCH_INDEX_FILE = f'{WEB_CACHE_DIR}/index.yml'
|
||||
SUGGESTIONS_CACHE_FILE = f'{WEB_CACHE_DIR}/suggestions.yml'
|
||||
SUGGESTIONS_CACHE_TS_FILE = map_timestamp_file(SUGGESTIONS_CACHE_FILE)
|
||||
CONFIG_FILE = f'{CONFIG_DIR}/web.yml'
|
||||
ENVIRONMENT_SETTINGS_CACHED_FILE = f'{WEB_CACHE_DIR}/environment.yml'
|
||||
ENVIRONMENT_SETTINGS_TS_FILE = f'{WEB_CACHE_DIR}/environment.ts'
|
||||
|
||||
@@ -6,10 +6,9 @@ import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import traceback
|
||||
from math import floor
|
||||
from pathlib import Path
|
||||
from threading import Thread
|
||||
from typing import List, Type, Set, Tuple, Optional, Dict, Generator, Iterable
|
||||
from typing import List, Type, Set, Tuple, Optional, Dict, Generator, Iterable, Pattern
|
||||
|
||||
import requests
|
||||
import yaml
|
||||
@@ -32,9 +31,8 @@ from bauh.commons.boot import CreateConfigFile
|
||||
from bauh.commons.html import bold
|
||||
from bauh.commons.system import ProcessHandler, get_dir_size, SimpleProcess
|
||||
from bauh.commons.view_utils import get_human_size_str
|
||||
from bauh.gems.web import INSTALLED_PATH, nativefier, DESKTOP_ENTRY_PATH_PATTERN, URL_FIX_PATTERN, ENV_PATH, UA_CHROME, \
|
||||
SUGGESTIONS_CACHE_FILE, ROOT_DIR, TEMP_PATH, FIX_FILE_PATH, ELECTRON_CACHE_DIR, \
|
||||
get_icon_path, URL_PROPS_PATTERN
|
||||
from bauh.gems.web import INSTALLED_PATH, nativefier, DESKTOP_ENTRY_PATH_PATTERN, URL_FIX_PATTERN, ENV_PATH, \
|
||||
ROOT_DIR, TEMP_PATH, FIX_FILE_PATH, ELECTRON_CACHE_DIR, UA_CHROME, get_icon_path, URL_PROPS_PATTERN
|
||||
from bauh.gems.web.config import WebConfigManager
|
||||
from bauh.gems.web.environment import EnvironmentUpdater, EnvironmentComponent
|
||||
from bauh.gems.web.model import WebApplication
|
||||
@@ -44,18 +42,18 @@ from bauh.gems.web.worker import SuggestionsManager, UpdateEnvironmentSettings,
|
||||
|
||||
try:
|
||||
from bs4 import BeautifulSoup, SoupStrainer
|
||||
|
||||
BS4_AVAILABLE = True
|
||||
except:
|
||||
BS4_AVAILABLE = False
|
||||
|
||||
|
||||
try:
|
||||
import lxml
|
||||
|
||||
LXML_AVAILABLE = True
|
||||
except:
|
||||
LXML_AVAILABLE = False
|
||||
|
||||
RE_PROTOCOL_STRIP = re.compile(r'[a-zA-Z]+://')
|
||||
RE_SEVERAL_SPACES = re.compile(r'\s+')
|
||||
RE_SYMBOLS_SPLIT = re.compile(r'[\-|_\s:.]')
|
||||
|
||||
@@ -64,7 +62,7 @@ DEFAULT_LANGUAGE_HEADER = 'en-US, en'
|
||||
|
||||
class WebApplicationManager(SoftwareManager, SettingsController):
|
||||
|
||||
def __init__(self, context: ApplicationContext, suggestions_loader: Optional[SuggestionsLoader] = None):
|
||||
def __init__(self, context: ApplicationContext):
|
||||
super(WebApplicationManager, self).__init__(context=context)
|
||||
self.http_client = context.http_client
|
||||
self.env_updater = EnvironmentUpdater(logger=context.logger, http_client=context.http_client,
|
||||
@@ -73,11 +71,13 @@ class WebApplicationManager(SoftwareManager, SettingsController):
|
||||
self.i18n = context.i18n
|
||||
self.logger = context.logger
|
||||
self.env_thread = None
|
||||
self.suggestions_loader = suggestions_loader
|
||||
self.suggestions_loader: Optional[SuggestionsLoader] = None
|
||||
self._suggestions_manager: Optional[SuggestionsManager] = None
|
||||
self.suggestions = {}
|
||||
self.configman = WebConfigManager()
|
||||
self.idxman = SearchIndexManager(logger=context.logger)
|
||||
self._custom_actions: Optional[Iterable[CustomSoftwareAction]] = None
|
||||
self._re_protocol_strip: Optional[Pattern] = None
|
||||
|
||||
def get_accept_language_header(self) -> str:
|
||||
try:
|
||||
@@ -174,7 +174,7 @@ class WebApplicationManager(SoftwareManager, SettingsController):
|
||||
if icon_url:
|
||||
return icon_url
|
||||
|
||||
def _get_app_description(self, url: str, soup: "BeautifulSoup") -> str:
|
||||
def _get_app_description(self, url: str, soup: "BeautifulSoup") -> str:
|
||||
description = None
|
||||
desc_tag = soup.head.find('meta', attrs={'name': 'description'})
|
||||
|
||||
@@ -227,13 +227,17 @@ class WebApplicationManager(SoftwareManager, SettingsController):
|
||||
return props
|
||||
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Error when trying to retrieve custom installation properties for {props_url}: {e.__class__.__name__}")
|
||||
self.logger.warning(
|
||||
f"Error when trying to retrieve custom installation properties for {props_url}: {e.__class__.__name__}")
|
||||
|
||||
def _map_electron_branch(self, version: str) -> str:
|
||||
return f"electron_{'_'.join(version.split('.')[0:-1])}_X"
|
||||
|
||||
def _strip_url_protocol(self, url: str) -> str:
|
||||
return RE_PROTOCOL_STRIP.split(url)[1].strip().lower()
|
||||
def strip_url_protocol(self, url: str) -> str:
|
||||
if not self._re_protocol_strip:
|
||||
self._re_protocol_strip = re.compile(r'^[a-zA-Z]+://(www\.)?')
|
||||
|
||||
return self._re_protocol_strip.split(url)[-1].strip().lower()
|
||||
|
||||
def serialize_to_disk(self, pkg: SoftwarePackage, icon_bytes: Optional[bytes], only_icon: bool):
|
||||
super(WebApplicationManager, self).serialize_to_disk(pkg=pkg, icon_bytes=None, only_icon=False)
|
||||
@@ -242,7 +246,8 @@ class WebApplicationManager(SoftwareManager, SettingsController):
|
||||
headers = {'Accept-language': self.get_accept_language_header(), 'User-Agent': UA_CHROME}
|
||||
|
||||
try:
|
||||
return self.http_client.get(url, headers=headers, ignore_ssl=True, single_call=True, session=False, allow_redirects=True)
|
||||
return self.http_client.get(url, headers=headers, ignore_ssl=True, single_call=True, session=False,
|
||||
allow_redirects=True)
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Could not GET '{url}'. Exception: {e.__class__.__name__}")
|
||||
|
||||
@@ -263,9 +268,9 @@ class WebApplicationManager(SoftwareManager, SettingsController):
|
||||
if is_url:
|
||||
url = words[0:-1] if words.endswith('/') else words
|
||||
|
||||
url_no_protocol = self._strip_url_protocol(url)
|
||||
url_no_protocol = self.strip_url_protocol(url)
|
||||
|
||||
installed_matches = [app for app in installed if self._strip_url_protocol(app.url) == url_no_protocol]
|
||||
installed_matches = [app for app in installed if self.strip_url_protocol(app.url) == url_no_protocol]
|
||||
|
||||
if installed_matches:
|
||||
res.installed.extend(installed_matches)
|
||||
@@ -320,25 +325,28 @@ class WebApplicationManager(SoftwareManager, SettingsController):
|
||||
self.logger.info("Query '{}' was not found in the suggestion's index".format(words))
|
||||
res.installed.extend(installed_matches)
|
||||
else:
|
||||
if not os.path.exists(SUGGESTIONS_CACHE_FILE):
|
||||
cached_file_path = self.suggestions_manager.get_cached_file_path()
|
||||
|
||||
if not os.path.exists(cached_file_path):
|
||||
# if the suggestions cache was not found, it will not be possible to retrieve the matched apps
|
||||
# so only the installed matches will be returned
|
||||
self.logger.warning("Suggestion cached file {} was not found".format(SUGGESTIONS_CACHE_FILE))
|
||||
self.logger.warning(f"Suggestion file {cached_file_path} was not found")
|
||||
res.installed.extend(installed_matches)
|
||||
else:
|
||||
with open(SUGGESTIONS_CACHE_FILE) as f:
|
||||
with open(cached_file_path) as f:
|
||||
cached_suggestions = yaml.safe_load(f.read())
|
||||
|
||||
if not cached_suggestions:
|
||||
# if no suggestion is found, it will not be possible to retrieve the matched apps
|
||||
# so only the installed matches will be returned
|
||||
self.logger.warning("No suggestion found in {}".format(SUGGESTIONS_CACHE_FILE))
|
||||
self.logger.warning(f"No suggestion found in {cached_file_path}")
|
||||
res.installed.extend(installed_matches)
|
||||
else:
|
||||
matched_suggestions = [cached_suggestions[key] for key in index_match_keys if cached_suggestions.get(key)]
|
||||
matched_suggestions = [cached_suggestions[key] for key in index_match_keys if
|
||||
cached_suggestions.get(key)]
|
||||
|
||||
if not matched_suggestions:
|
||||
self.logger.warning("No suggestion found for the search index keys: {}".format(index_match_keys))
|
||||
self.logger.warning(f"No suggestion found for the query index keys: {index_match_keys}")
|
||||
res.installed.extend(installed_matches)
|
||||
else:
|
||||
matched_suggestions.sort(key=lambda s: s.get('priority', 0), reverse=True)
|
||||
@@ -376,7 +384,8 @@ class WebApplicationManager(SoftwareManager, SettingsController):
|
||||
|
||||
return res
|
||||
|
||||
def read_installed(self, disk_loader: Optional[DiskCacheLoader], limit: int = -1, only_apps: bool = False, pkg_types: Set[Type[SoftwarePackage]] = None, internet_available: bool = True) -> SearchResult:
|
||||
def read_installed(self, disk_loader: Optional[DiskCacheLoader], limit: int = -1, only_apps: bool = False,
|
||||
pkg_types: Set[Type[SoftwarePackage]] = None, internet_available: bool = True) -> SearchResult:
|
||||
res = SearchResult([], [], 0)
|
||||
|
||||
if os.path.exists(INSTALLED_PATH):
|
||||
@@ -393,12 +402,14 @@ class WebApplicationManager(SoftwareManager, SettingsController):
|
||||
def upgrade(self, requirements: UpgradeRequirements, root_password: Optional[str], watcher: ProcessWatcher) -> bool:
|
||||
pass
|
||||
|
||||
def uninstall(self, pkg: WebApplication, root_password: Optional[str], watcher: ProcessWatcher, disk_loader: DiskCacheLoader) -> TransactionResult:
|
||||
def uninstall(self, pkg: WebApplication, root_password: Optional[str], watcher: ProcessWatcher,
|
||||
disk_loader: DiskCacheLoader) -> TransactionResult:
|
||||
self.logger.info("Checking if {} installation directory {} exists".format(pkg.name, pkg.installation_dir))
|
||||
|
||||
if not os.path.exists(pkg.installation_dir):
|
||||
watcher.show_message(title=self.i18n['error'],
|
||||
body=self.i18n['web.uninstall.error.install_dir.not_found'].format(bold(pkg.installation_dir)),
|
||||
body=self.i18n['web.uninstall.error.install_dir.not_found'].format(
|
||||
bold(pkg.installation_dir)),
|
||||
type_=MessageType.ERROR)
|
||||
return TransactionResult.fail()
|
||||
|
||||
@@ -464,7 +475,8 @@ class WebApplicationManager(SoftwareManager, SettingsController):
|
||||
|
||||
def get_info(self, pkg: WebApplication) -> dict:
|
||||
if pkg.installed:
|
||||
info = {'0{}_{}'.format(idx + 1, att): getattr(pkg, att) for idx, att in enumerate(('url', 'description', 'version', 'categories', 'installation_dir', 'desktop_entry'))}
|
||||
info = {'0{}_{}'.format(idx + 1, att): getattr(pkg, att) for idx, att in
|
||||
enumerate(('url', 'description', 'version', 'categories', 'installation_dir', 'desktop_entry'))}
|
||||
info['07_exec_file'] = pkg.get_exec_path()
|
||||
info['08_icon_path'] = pkg.get_disk_icon_path()
|
||||
|
||||
@@ -481,12 +493,14 @@ class WebApplicationManager(SoftwareManager, SettingsController):
|
||||
|
||||
return info
|
||||
else:
|
||||
return {'0{}_{}'.format(idx + 1, att): getattr(pkg, att) for idx, att in enumerate(('url', 'description', 'version', 'categories'))}
|
||||
return {'0{}_{}'.format(idx + 1, att): getattr(pkg, att) for idx, att in
|
||||
enumerate(('url', 'description', 'version', 'categories'))}
|
||||
|
||||
def get_history(self, pkg: SoftwarePackage) -> PackageHistory:
|
||||
pass
|
||||
|
||||
def _ask_install_options(self, app: WebApplication, watcher: ProcessWatcher, pre_validated: bool) -> Tuple[bool, List[str]]:
|
||||
def _ask_install_options(self, app: WebApplication, watcher: ProcessWatcher, pre_validated: bool) -> Tuple[
|
||||
bool, List[str]]:
|
||||
watcher.change_substatus(self.i18n['web.install.substatus.options'])
|
||||
|
||||
max_width = 350
|
||||
@@ -703,9 +717,11 @@ class WebApplicationManager(SoftwareManager, SettingsController):
|
||||
self.logger.error("An exception has happened when downloading {}".format(pkg.icon_url))
|
||||
traceback.print_exc()
|
||||
else:
|
||||
self.logger.warning('Could no retrieve the icon {} defined for the suggestion {}'.format(pkg.icon_url, pkg.name))
|
||||
self.logger.warning(
|
||||
'Could no retrieve the icon {} defined for the suggestion {}'.format(pkg.icon_url, pkg.name))
|
||||
except:
|
||||
self.logger.warning('An exception happened when trying to retrieve the icon {} for the suggestion {}'.format(pkg.icon_url,
|
||||
self.logger.warning(
|
||||
'An exception happened when trying to retrieve the icon {} for the suggestion {}'.format(pkg.icon_url,
|
||||
pkg.name))
|
||||
traceback.print_exc()
|
||||
|
||||
@@ -749,7 +765,7 @@ class WebApplicationManager(SoftwareManager, SettingsController):
|
||||
|
||||
electron_version = str(next((c for c in env_components if c.id == 'electron')).version)
|
||||
|
||||
url_domain, electron_branch = self._strip_url_protocol(pkg.url), self._map_electron_branch(electron_version)
|
||||
url_domain, electron_branch = self.strip_url_protocol(pkg.url), self._map_electron_branch(electron_version)
|
||||
fix = self._get_fix_for(url_domain=url_domain, electron_branch=electron_branch)
|
||||
|
||||
if fix:
|
||||
@@ -886,7 +902,8 @@ class WebApplicationManager(SoftwareManager, SettingsController):
|
||||
|
||||
return TransactionResult(success=True, installed=[pkg], removed=[])
|
||||
|
||||
def install(self, pkg: WebApplication, root_password: Optional[str], disk_loader: DiskCacheLoader, watcher: ProcessWatcher) -> TransactionResult:
|
||||
def install(self, pkg: WebApplication, root_password: Optional[str], disk_loader: DiskCacheLoader,
|
||||
watcher: ProcessWatcher) -> TransactionResult:
|
||||
continue_install, install_options = self._ask_install_options(pkg, watcher, pre_validated=True)
|
||||
|
||||
if not continue_install:
|
||||
@@ -967,9 +984,7 @@ class WebApplicationManager(SoftwareManager, SettingsController):
|
||||
create_config=create_config,
|
||||
i18n=self.i18n).start()
|
||||
|
||||
self.suggestions_loader = SuggestionsLoader(manager=SuggestionsManager(logger=self.logger,
|
||||
http_client=self.http_client,
|
||||
i18n=self.i18n),
|
||||
self.suggestions_loader = SuggestionsLoader(manager=self.suggestions_manager,
|
||||
logger=self.logger,
|
||||
i18n=self.i18n,
|
||||
taskman=task_manager,
|
||||
@@ -1008,7 +1023,8 @@ class WebApplicationManager(SoftwareManager, SettingsController):
|
||||
app.description = self._get_app_description(app.url, soup)
|
||||
|
||||
try:
|
||||
find_url = not app.icon_url or (app.icon_url and not self.http_client.exists(app.icon_url, session=False))
|
||||
find_url = not app.icon_url or (
|
||||
app.icon_url and not self.http_client.exists(app.icon_url, session=False))
|
||||
except (requests.exceptions.ConnectionError, requests.exceptions.ConnectTimeout):
|
||||
find_url = None
|
||||
|
||||
@@ -1049,6 +1065,9 @@ class WebApplicationManager(SoftwareManager, SettingsController):
|
||||
output.update(self.configman.get_config())
|
||||
|
||||
def list_suggestions(self, limit: int, filter_installed: bool) -> Optional[List[PackageSuggestion]]:
|
||||
if limit == 0:
|
||||
return
|
||||
|
||||
web_config = {}
|
||||
|
||||
thread_config = Thread(target=self._fill_config_async, args=(web_config,))
|
||||
@@ -1057,10 +1076,19 @@ class WebApplicationManager(SoftwareManager, SettingsController):
|
||||
if self.suggestions:
|
||||
suggestions = self.suggestions
|
||||
elif self.suggestions_loader:
|
||||
self.suggestions_loader.join(5)
|
||||
if self.suggestions_loader.is_alive():
|
||||
self.suggestions_loader.join(5)
|
||||
|
||||
suggestions = self.suggestions
|
||||
elif self.suggestions_manager.is_custom_local_file_mapped():
|
||||
suggestions = self.suggestions_manager.read_cached()
|
||||
else:
|
||||
suggestions = SuggestionsManager(logger=self.logger, http_client=self.http_client, i18n=self.i18n).download()
|
||||
thread_config.join()
|
||||
|
||||
if self.suggestions_manager.should_download(web_config):
|
||||
suggestions = self.suggestions_manager.download()
|
||||
else:
|
||||
suggestions = self.suggestions_manager.read_cached()
|
||||
|
||||
# cleaning memory
|
||||
self.suggestions_loader = None
|
||||
@@ -1071,22 +1099,24 @@ class WebApplicationManager(SoftwareManager, SettingsController):
|
||||
suggestion_list.sort(key=lambda s: s.get('priority', 0), reverse=True)
|
||||
|
||||
if filter_installed:
|
||||
installed = {self._strip_url_protocol(i.url) for i in self.read_installed(disk_loader=None).installed}
|
||||
installed = {self.strip_url_protocol(i.url) for i in self.read_installed(disk_loader=None).installed}
|
||||
else:
|
||||
installed = None
|
||||
|
||||
env_settings, res = None, []
|
||||
|
||||
for s in suggestion_list:
|
||||
if limit <= 0 or len(res) < limit:
|
||||
if limit < 0 or len(res) < limit:
|
||||
if installed:
|
||||
surl = self._strip_url_protocol(s['url'])
|
||||
surl = self.strip_url_protocol(s['url'])
|
||||
|
||||
if surl in installed:
|
||||
continue
|
||||
|
||||
if env_settings is None: # reading settings if not already loaded
|
||||
thread_config.join()
|
||||
if thread_config.is_alive():
|
||||
thread_config.join()
|
||||
|
||||
env_settings = self.env_updater.read_settings(web_config=web_config)
|
||||
|
||||
res.append(self._map_suggestion(s, env_settings))
|
||||
@@ -1107,7 +1137,8 @@ class WebApplicationManager(SoftwareManager, SettingsController):
|
||||
|
||||
return res
|
||||
|
||||
def execute_custom_action(self, action: CustomSoftwareAction, pkg: SoftwarePackage, root_password: Optional[str], watcher: ProcessWatcher) -> bool:
|
||||
def execute_custom_action(self, action: CustomSoftwareAction, pkg: SoftwarePackage, root_password: Optional[str],
|
||||
watcher: ProcessWatcher) -> bool:
|
||||
pass
|
||||
|
||||
def is_default_enabled(self) -> bool:
|
||||
@@ -1127,7 +1158,8 @@ class WebApplicationManager(SoftwareManager, SettingsController):
|
||||
print('{}[bauh][web] Directory {} deleted{}'.format(Fore.YELLOW, ENV_PATH, Fore.RESET))
|
||||
except:
|
||||
if logs:
|
||||
print('{}[bauh][web] An exception has happened when deleting {}{}'.format(Fore.RED, ENV_PATH, Fore.RESET))
|
||||
print('{}[bauh][web] An exception has happened when deleting {}{}'.format(Fore.RED, ENV_PATH,
|
||||
Fore.RESET))
|
||||
traceback.print_exc()
|
||||
|
||||
def get_settings(self) -> Optional[Generator[SettingsView, None, None]]:
|
||||
@@ -1140,13 +1172,16 @@ class WebApplicationManager(SoftwareManager, SettingsController):
|
||||
id_='electron_branch')
|
||||
|
||||
native_opts = [
|
||||
InputOption(label=self.i18n['web.settings.nativefier.env'].capitalize(), value=False, tooltip=self.i18n['web.settings.nativefier.env.tooltip'].format(app=self.context.app_name)),
|
||||
InputOption(label=self.i18n['web.settings.nativefier.system'].capitalize(), value=True, tooltip=self.i18n['web.settings.nativefier.system.tooltip'])
|
||||
InputOption(label=self.i18n['web.settings.nativefier.env'].capitalize(), value=False,
|
||||
tooltip=self.i18n['web.settings.nativefier.env.tooltip'].format(app=self.context.app_name)),
|
||||
InputOption(label=self.i18n['web.settings.nativefier.system'].capitalize(), value=True,
|
||||
tooltip=self.i18n['web.settings.nativefier.system.tooltip'])
|
||||
]
|
||||
|
||||
select_nativefier = SingleSelectComponent(label="Nativefier",
|
||||
options=native_opts,
|
||||
default_option=[o for o in native_opts if o.value == web_config['environment']['system']][0],
|
||||
default_option=[o for o in native_opts if
|
||||
o.value == web_config['environment']['system']][0],
|
||||
type_=SelectViewType.COMBO,
|
||||
tooltip=self.i18n['web.settings.nativefier.tip'],
|
||||
alignment=ViewComponentAlignment.CENTER,
|
||||
@@ -1155,7 +1190,8 @@ class WebApplicationManager(SoftwareManager, SettingsController):
|
||||
env_settings_exp = TextInputComponent(label=self.i18n['web.settings.cache_exp'],
|
||||
tooltip=self.i18n['web.settings.cache_exp.tip'],
|
||||
capitalize_label=False,
|
||||
value=int(web_config['environment']['cache_exp']) if isinstance(web_config['environment']['cache_exp'], int) else '',
|
||||
value=int(web_config['environment']['cache_exp']) if isinstance(
|
||||
web_config['environment']['cache_exp'], int) else '',
|
||||
only_int=True,
|
||||
id_='web_cache_exp')
|
||||
|
||||
@@ -1221,3 +1257,13 @@ class WebApplicationManager(SoftwareManager, SettingsController):
|
||||
)
|
||||
|
||||
yield from self._custom_actions
|
||||
|
||||
@property
|
||||
def suggestions_manager(self):
|
||||
if self._suggestions_manager is None:
|
||||
self._suggestions_manager = SuggestionsManager(logger=self.logger,
|
||||
http_client=self.http_client,
|
||||
i18n=self.i18n,
|
||||
file_url=self.context.get_suggestion_url(self.__module__))
|
||||
|
||||
return self._suggestions_manager
|
||||
|
||||
@@ -3,23 +3,44 @@ import traceback
|
||||
from datetime import datetime, timedelta
|
||||
from logging import Logger
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
import yaml
|
||||
|
||||
from bauh.api.http import HttpClient
|
||||
from bauh.gems.web import URL_SUGGESTIONS, SUGGESTIONS_CACHE_FILE, SUGGESTIONS_CACHE_TS_FILE
|
||||
from bauh.commons.util import map_timestamp_file
|
||||
from bauh.gems.web import WEB_CACHE_DIR
|
||||
from bauh.view.util.translation import I18n
|
||||
|
||||
|
||||
class SuggestionsManager:
|
||||
|
||||
def __init__(self, http_client: HttpClient, logger: Logger, i18n: I18n):
|
||||
def __init__(self, http_client: HttpClient, logger: Logger, i18n: I18n, file_url: Optional[str]):
|
||||
self.http_client = http_client
|
||||
self.logger = logger
|
||||
self.i18n = i18n
|
||||
if file_url:
|
||||
self._file_url = file_url
|
||||
else:
|
||||
self._file_url = "https://raw.githubusercontent.com/vinifmor/bauh-files/master/web/env/v2/suggestions.yml"
|
||||
self._cached_file_path = f'{WEB_CACHE_DIR}/suggestions.yml'
|
||||
self._cached_file_ts_path = map_timestamp_file(self._cached_file_path)
|
||||
|
||||
@property
|
||||
def file_url(self) -> Optional[str]:
|
||||
return self._file_url
|
||||
|
||||
def is_custom_local_file_mapped(self) -> bool:
|
||||
return self._file_url and self._file_url.startswith('/')
|
||||
|
||||
def should_download(self, web_config: dict) -> bool:
|
||||
if not self._file_url:
|
||||
return False
|
||||
|
||||
if self.is_custom_local_file_mapped():
|
||||
return False
|
||||
|
||||
exp = web_config['suggestions']['cache_exp']
|
||||
if web_config['suggestions']['cache_exp'] is None:
|
||||
self.logger.info("No cache expiration defined for suggestions")
|
||||
@@ -28,28 +49,28 @@ class SuggestionsManager:
|
||||
try:
|
||||
exp = int(exp)
|
||||
except ValueError:
|
||||
self.logger.error("Error while parsing the 'suggestions.cache_exp' ({}) settings property".format(exp))
|
||||
self.logger.error(f"Error while parsing the 'suggestions.cache_exp' ({exp}) settings property")
|
||||
return True
|
||||
|
||||
if exp <= 0:
|
||||
self.logger.info("No cache expiration defined for suggestions ({})".format(exp))
|
||||
self.logger.info(f"No cache expiration defined for suggestions ({exp})")
|
||||
return True
|
||||
|
||||
if not os.path.exists(SUGGESTIONS_CACHE_FILE):
|
||||
self.logger.info("No suggestions cached file found '{}'".format(SUGGESTIONS_CACHE_FILE))
|
||||
if not os.path.exists(self._cached_file_path):
|
||||
self.logger.info(f"No suggestions cached file found '{self._cached_file_path}'")
|
||||
return True
|
||||
|
||||
if not os.path.exists(SUGGESTIONS_CACHE_TS_FILE):
|
||||
self.logger.info("No suggestions cache file timestamp found '{}'".format(SUGGESTIONS_CACHE_TS_FILE))
|
||||
if not os.path.exists(self._cached_file_ts_path):
|
||||
self.logger.info(f"No suggestions cache file timestamp found '{self._cached_file_ts_path}'")
|
||||
return True
|
||||
|
||||
with open(SUGGESTIONS_CACHE_TS_FILE) as f:
|
||||
with open(self._cached_file_ts_path) as f:
|
||||
timestamp_str = f.read()
|
||||
|
||||
try:
|
||||
sugs_timestamp = datetime.fromtimestamp(float(timestamp_str))
|
||||
except:
|
||||
self.logger.error("Could not parse the cached suggestions file timestamp: {}".format(timestamp_str))
|
||||
self.logger.error(f"Could not parse the cached suggestions file timestamp: {timestamp_str}")
|
||||
return True
|
||||
|
||||
expired = sugs_timestamp + timedelta(days=exp) <= datetime.utcnow()
|
||||
@@ -61,17 +82,25 @@ class SuggestionsManager:
|
||||
self.logger.info("Cached suggestions file is up to date")
|
||||
return False
|
||||
|
||||
def get_cached_file_path(self) -> str:
|
||||
return self._file_url if self.is_custom_local_file_mapped() else self._cached_file_path
|
||||
|
||||
def read_cached(self, check_file: bool = True) -> dict:
|
||||
if check_file and not os.path.exists(SUGGESTIONS_CACHE_FILE):
|
||||
self.logger.warning("Cached suggestions file does not exist ({})".format(SUGGESTIONS_CACHE_FILE))
|
||||
if self.is_custom_local_file_mapped():
|
||||
file_path, log_ref = self._file_url, 'local'
|
||||
else:
|
||||
file_path, log_ref = self._cached_file_path, 'cached'
|
||||
|
||||
if check_file and not os.path.exists(file_path):
|
||||
self.logger.warning(f"{log_ref.capitalize()} suggestions file does not exist ({file_path})")
|
||||
return {}
|
||||
|
||||
self.logger.info("Reading cached suggestions file '{}'".format(SUGGESTIONS_CACHE_FILE))
|
||||
with open(SUGGESTIONS_CACHE_FILE) as f:
|
||||
self.logger.info(f"Reading {log_ref} suggestions file '{file_path}'")
|
||||
with open(file_path) as f:
|
||||
sugs_str = f.read()
|
||||
|
||||
if not sugs_str:
|
||||
self.logger.warning("Cached suggestions file '{}' is empty".format(SUGGESTIONS_CACHE_FILE))
|
||||
self.logger.warning(f"{log_ref.capitalize()} suggestions file '{file_path}' is empty")
|
||||
return {}
|
||||
|
||||
try:
|
||||
@@ -82,14 +111,14 @@ class SuggestionsManager:
|
||||
return {}
|
||||
|
||||
def download(self) -> dict:
|
||||
self.logger.info("Reading suggestions from {}".format(URL_SUGGESTIONS))
|
||||
self.logger.info(f"Reading suggestions from {self._file_url}")
|
||||
try:
|
||||
suggestions = self.http_client.get_yaml(URL_SUGGESTIONS, session=False)
|
||||
suggestions = self.http_client.get_yaml(self._file_url, session=False)
|
||||
|
||||
if suggestions:
|
||||
self.logger.info("{} suggestions successfully read".format(len(suggestions)))
|
||||
self.logger.info(f"{len(suggestions)} suggestions successfully read")
|
||||
else:
|
||||
self.logger.warning("Could not read suggestions from {}".format(URL_SUGGESTIONS))
|
||||
self.logger.warning(f"Could not read suggestions from {self._file_url}")
|
||||
|
||||
except (requests.exceptions.ConnectionError, requests.exceptions.ConnectTimeout):
|
||||
self.logger.warning("Internet seems to be off: it was not possible to retrieve the suggestions")
|
||||
@@ -102,31 +131,36 @@ class SuggestionsManager:
|
||||
if not suggestions:
|
||||
return
|
||||
|
||||
self.logger.info('Caching {} suggestions to the disk'.format(len(suggestions)))
|
||||
suggestions_file_dir = os.path.dirname(SUGGESTIONS_CACHE_FILE)
|
||||
if self.is_custom_local_file_mapped():
|
||||
return
|
||||
|
||||
self.logger.info(f'Caching {len(suggestions)} suggestions to the disk')
|
||||
suggestions_file_dir = os.path.dirname(self._cached_file_path)
|
||||
|
||||
try:
|
||||
Path(suggestions_file_dir).mkdir(parents=True, exist_ok=True)
|
||||
except OSError:
|
||||
self.logger.error("Could not generate the directory {}".format(suggestions_file_dir))
|
||||
self.logger.error(f"Could not generate the directory {suggestions_file_dir}")
|
||||
traceback.print_exc()
|
||||
return
|
||||
|
||||
try:
|
||||
with open(SUGGESTIONS_CACHE_FILE, 'w+') as f:
|
||||
with open(self._cached_file_path, 'w+') as f:
|
||||
f.write(yaml.safe_dump(suggestions))
|
||||
except:
|
||||
self.logger.error("Could write to {}".format(SUGGESTIONS_CACHE_FILE))
|
||||
self.logger.error(f"Could write to {self._cached_file_path}")
|
||||
traceback.print_exc()
|
||||
return
|
||||
|
||||
self.logger.info("{} suggestions successfully cached to file '{}'".format(len(suggestions), SUGGESTIONS_CACHE_FILE))
|
||||
self.logger.info(f"{len(suggestions)} suggestions successfully cached to file '{self._cached_file_path}'")
|
||||
|
||||
try:
|
||||
with open(SUGGESTIONS_CACHE_TS_FILE, 'w+') as f:
|
||||
with open(self._cached_file_ts_path, 'w+') as f:
|
||||
f.write(str(timestamp))
|
||||
except:
|
||||
self.logger.error("Could not write to {}".format(SUGGESTIONS_CACHE_TS_FILE))
|
||||
self.logger.error(f"Could not write to {self._cached_file_ts_path}")
|
||||
traceback.print_exc()
|
||||
return
|
||||
|
||||
self.logger.info("Suggestions cached file timestamp ({}) successfully saved at '{}'".format(timestamp, SUGGESTIONS_CACHE_TS_FILE))
|
||||
self.logger.info(f"Suggestions cached file timestamp ({timestamp}) "
|
||||
f"successfully saved at '{self._cached_file_ts_path}'")
|
||||
|
||||
@@ -35,27 +35,33 @@ class SuggestionsLoader(Thread):
|
||||
|
||||
def run(self):
|
||||
ti = time.time()
|
||||
self.taskman.update_progress(self.task_id, 0, self.i18n['task.waiting_task'].format(bold(self.create_config.task_name)))
|
||||
self.create_config.join()
|
||||
|
||||
self.taskman.update_progress(self.task_id, 10, None)
|
||||
|
||||
if not self.internet_connection:
|
||||
self.logger.warning("No internet connection. Only cached suggestions can be loaded")
|
||||
self.suggestions = self.manager.read_cached(check_file=True)
|
||||
elif not self.manager.should_download(self.create_config.config):
|
||||
self.suggestions = self.manager.read_cached(check_file=False)
|
||||
if self.manager.is_custom_local_file_mapped():
|
||||
self.taskman.update_progress(self.task_id, 50, None)
|
||||
self.logger.info(f"Local Web suggestions file mapped: {self.manager.file_url}")
|
||||
self.suggestions = self.manager.read_cached()
|
||||
else:
|
||||
try:
|
||||
timestamp = datetime.utcnow().timestamp()
|
||||
self.suggestions = self.manager.download()
|
||||
wait_msg = self.i18n['task.waiting_task'].format(bold(self.create_config.task_name))
|
||||
self.taskman.update_progress(self.task_id, 0, wait_msg)
|
||||
self.create_config.join()
|
||||
|
||||
if self.suggestions:
|
||||
self.taskman.update_progress(self.task_id, 50, self.i18n['web.task.suggestions.saving'])
|
||||
self.manager.save_to_disk(self.suggestions, timestamp)
|
||||
except:
|
||||
self.logger.error("Unexpected exception")
|
||||
traceback.print_exc()
|
||||
self.taskman.update_progress(self.task_id, 10, None)
|
||||
|
||||
if not self.internet_connection:
|
||||
self.logger.warning("No internet connection. Only cached suggestions can be loaded")
|
||||
self.suggestions = self.manager.read_cached()
|
||||
elif not self.manager.should_download(self.create_config.config):
|
||||
self.suggestions = self.manager.read_cached(check_file=False)
|
||||
else:
|
||||
try:
|
||||
timestamp = datetime.utcnow().timestamp()
|
||||
self.suggestions = self.manager.download()
|
||||
|
||||
if self.suggestions:
|
||||
self.taskman.update_progress(self.task_id, 50, self.i18n['web.task.suggestions.saving'])
|
||||
self.manager.save_to_disk(self.suggestions, timestamp)
|
||||
except:
|
||||
self.logger.error("Unexpected exception")
|
||||
traceback.print_exc()
|
||||
|
||||
if self.suggestions_callback:
|
||||
self.taskman.update_progress(self.task_id, 75, None)
|
||||
@@ -73,7 +79,8 @@ class SuggestionsLoader(Thread):
|
||||
|
||||
class SearchIndexGenerator(Thread):
|
||||
|
||||
def __init__(self, taskman: TaskManager, idxman: SearchIndexManager, suggestions_loader: SuggestionsLoader, i18n: I18n, logger: logging.Logger):
|
||||
def __init__(self, taskman: TaskManager, idxman: SearchIndexManager, suggestions_loader: SuggestionsLoader,
|
||||
i18n: I18n, logger: logging.Logger):
|
||||
super(SearchIndexGenerator, self).__init__(daemon=True)
|
||||
self.taskman = taskman
|
||||
self.idxman = idxman
|
||||
@@ -85,8 +92,11 @@ class SearchIndexGenerator(Thread):
|
||||
|
||||
def run(self):
|
||||
ti = time.time()
|
||||
self.taskman.update_progress(self.task_id, 0, self.i18n['task.waiting_task'].format(bold(self.suggestions_loader.task_name)))
|
||||
self.suggestions_loader.join()
|
||||
|
||||
if self.suggestions_loader.is_alive():
|
||||
wait_msg = self.i18n['task.waiting_task'].format(bold(self.suggestions_loader.task_name))
|
||||
self.taskman.update_progress(self.task_id, 0, wait_msg)
|
||||
self.suggestions_loader.join()
|
||||
|
||||
if self.suggestions_loader.suggestions:
|
||||
self.taskman.update_progress(self.task_id, 1, None)
|
||||
|
||||
Reference in New Issue
Block a user