diff --git a/bauh/__init__.py b/bauh/__init__.py index d3a4efa3..7289277f 100644 --- a/bauh/__init__.py +++ b/bauh/__init__.py @@ -1,4 +1,4 @@ -__version__ = '0.7.4' +__version__ = '0.8.0' __app_name__ = 'bauh' import os diff --git a/bauh/api/abstract/controller.py b/bauh/api/abstract/controller.py index 41d9abd1..1138bd02 100644 --- a/bauh/api/abstract/controller.py +++ b/bauh/api/abstract/controller.py @@ -37,11 +37,12 @@ class SoftwareManager(ABC): self.context = context @abstractmethod - def search(self, words: str, disk_loader: DiskCacheLoader, limit: int) -> SearchResult: + def search(self, words: str, disk_loader: DiskCacheLoader, limit: int, is_url: bool) -> SearchResult: """ :param words: the words typed by the user :param disk_loader: a running disk loader thread that loads package data from the disk asynchronously :param limit: the max number of packages to be retrieved. <= 1 should retrieve everything + :param is_url: if "words" is a URL :return: """ pass diff --git a/bauh/gems/appimage/controller.py b/bauh/gems/appimage/controller.py index 77aef8f1..242a6a6a 100644 --- a/bauh/gems/appimage/controller.py +++ b/bauh/gems/appimage/controller.py @@ -61,7 +61,10 @@ class AppImageManager(SoftwareManager): def _gen_app_key(self, app: AppImage): return '{}{}'.format(app.name.lower(), app.github.lower() if app.github else '') - def search(self, words: str, disk_loader: DiskCacheLoader, limit: int = -1) -> SearchResult: + def search(self, words: str, disk_loader: DiskCacheLoader, limit: int = -1, is_url: bool = False) -> SearchResult: + if is_url: + return SearchResult([], [], 0) + res = SearchResult([], [], 0) connection = self._get_db_connection(DB_APPS_PATH) diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index 87ce01a8..7dfe6d1a 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -79,7 +79,10 @@ class ArchManager(SoftwareManager): Thread(target=self.mapper.fill_package_build, args=(app,), daemon=True).start() - def search(self, words: str, disk_loader: DiskCacheLoader, limit: int = -1) -> SearchResult: + def search(self, words: str, disk_loader: DiskCacheLoader, limit: int = -1, is_url: bool = False) -> SearchResult: + if is_url: + return SearchResult([], [], 0) + self.comp_optimizer.join() downgrade_enabled = git.is_enabled() diff --git a/bauh/gems/flatpak/controller.py b/bauh/gems/flatpak/controller.py index 2227ae7a..5b9d7b39 100644 --- a/bauh/gems/flatpak/controller.py +++ b/bauh/gems/flatpak/controller.py @@ -56,7 +56,9 @@ class FlatpakManager(SoftwareManager): return app - def search(self, words: str, disk_loader: DiskCacheLoader, limit: int = -1) -> SearchResult: + def search(self, words: str, disk_loader: DiskCacheLoader, limit: int = -1, is_url: bool = False) -> SearchResult: + if is_url: + return SearchResult([], [], 0) res = SearchResult([], [], 0) apps_found = flatpak.search(flatpak.get_version(), words) diff --git a/bauh/gems/snap/controller.py b/bauh/gems/snap/controller.py index 37489422..608740ff 100644 --- a/bauh/gems/snap/controller.py +++ b/bauh/gems/snap/controller.py @@ -84,7 +84,10 @@ class SnapManager(SoftwareManager): return app - def search(self, words: str, disk_loader: DiskCacheLoader, limit: int = -1) -> SearchResult: + def search(self, words: str, disk_loader: DiskCacheLoader, limit: int = -1, is_url: bool = False) -> SearchResult: + if is_url: + return SearchResult([], [], 0) + if snap.is_snapd_running(): installed = self.read_installed(disk_loader).installed diff --git a/bauh/gems/snap/snap.py b/bauh/gems/snap/snap.py index ad73dcc1..e0fb2689 100644 --- a/bauh/gems/snap/snap.py +++ b/bauh/gems/snap/snap.py @@ -13,7 +13,7 @@ SNAPD_RUNNING_STATUS = {'listening', 'running'} def is_installed(): - res = run_cmd('which snap') + res = run_cmd('which snap', print_error=False) return res and not res.strip().startswith('which ') diff --git a/bauh/gems/web/__init__.py b/bauh/gems/web/__init__.py new file mode 100644 index 00000000..81c0edfd --- /dev/null +++ b/bauh/gems/web/__init__.py @@ -0,0 +1,3 @@ +import os + +ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) \ No newline at end of file diff --git a/bauh/gems/web/controller.py b/bauh/gems/web/controller.py new file mode 100644 index 00000000..97f7ab0a --- /dev/null +++ b/bauh/gems/web/controller.py @@ -0,0 +1,117 @@ +from typing import List, Type, Set + +from bauh.api.abstract.context import ApplicationContext +from bauh.api.abstract.controller import SoftwareManager, SearchResult +from bauh.api.abstract.disk import DiskCacheLoader +from bauh.api.abstract.handler import ProcessWatcher +from bauh.api.abstract.model import SoftwarePackage, PackageAction, PackageSuggestion, PackageUpdate, PackageHistory +from bauh.gems.web import npm +from bauh.gems.web.model import WebApplication + +try: + from bs4 import BeautifulSoup, SoupStrainer + BS4_AVAILABLE = True +except: + BS4_AVAILABLE = False + + +try: + import lxml + LXML_AVAILABLE = True +except: + LXML_AVAILABLE = False + + +class WebApplicationManager(SoftwareManager): + + def __init__(self, context: ApplicationContext): + super(WebApplicationManager, self).__init__(context=context) + self.http_client = context.http_client + self.enabled = True + + def search(self, words: str, disk_loader: DiskCacheLoader, limit: int = -1, is_url: bool = False) -> SearchResult: + res = SearchResult([], [], 0) + + if is_url: + url_res = self.http_client.get(words) + + if url_res: + soup = BeautifulSoup(url_res.text, 'lxml', parse_only=SoupStrainer('head')) + + name_tag = soup.head.find('meta', attrs={'name': 'application-name'}) + name = name_tag.get('content') if name_tag else words.split('.')[0] + + desc_tag = soup.head.find('meta', attrs={'name': 'description'}) + desc = desc_tag.get('content') if desc_tag else words + + icon_tag = soup.head.find('link', attrs={"rel": "icon"}) + icon_url = icon_tag.get('href') if icon_tag else None + + res.new = [WebApplication(url=words, name=name, description=desc, icon_url=icon_url)] + res.total = 1 + else: + # TODO + pass + + return res + + def read_installed(self, disk_loader: DiskCacheLoader, limit: int = -1, only_apps: bool = False, pkg_types: Set[Type[SoftwarePackage]] = None, internet_available: bool = True) -> SearchResult: + # TODO + return SearchResult([], [], 0) + + def downgrade(self, pkg: SoftwarePackage, root_password: str, handler: ProcessWatcher) -> bool: + pass + + def update(self, pkg: SoftwarePackage, root_password: str, watcher: ProcessWatcher) -> bool: + pass + + def uninstall(self, pkg: SoftwarePackage, root_password: str, watcher: ProcessWatcher) -> bool: + pass + + def get_managed_types(self) -> Set[Type[SoftwarePackage]]: + return {WebApplication} + + def get_info(self, pkg: SoftwarePackage) -> dict: + pass + + def get_history(self, pkg: SoftwarePackage) -> PackageHistory: + pass + + def install(self, pkg: SoftwarePackage, root_password: str, watcher: ProcessWatcher) -> bool: + pass + + def is_enabled(self) -> bool: + return self.enabled + + def set_enabled(self, enabled: bool): + self.enabled = enabled + + def can_work(self) -> bool: + return BS4_AVAILABLE and LXML_AVAILABLE and npm.is_available() + + def requires_root(self, action: str, pkg: SoftwarePackage): + return False + + def prepare(self): + pass + + def list_updates(self, internet_available: bool) -> List[PackageUpdate]: + pass + + def list_warnings(self, internet_available: bool) -> List[str]: + pass + + def list_suggestions(self, limit: int) -> List[PackageSuggestion]: + pass + + def execute_custom_action(self, action: PackageAction, pkg: SoftwarePackage, root_password: str, watcher: ProcessWatcher) -> bool: + pass + + def is_default_enabled(self) -> bool: + return True + + def launch(self, pkg: SoftwarePackage): + pass + + def get_screenshots(self, pkg: SoftwarePackage) -> List[str]: + pass diff --git a/bauh/gems/web/model.py b/bauh/gems/web/model.py new file mode 100644 index 00000000..43f0c7b4 --- /dev/null +++ b/bauh/gems/web/model.py @@ -0,0 +1,59 @@ +from bauh.api.abstract.model import SoftwarePackage +from bauh.api.constants import CACHE_PATH +from bauh.commons import resource +from bauh.gems.web import ROOT_DIR + + +class WebApplication(SoftwarePackage): + + def __init__(self, url: str, name: str, description: str, icon_url: str): + super(WebApplication, self).__init__(id=url, name=name, description=description, icon_url=icon_url) + self.url = url + + def has_history(self): + return False + + def has_info(self): + return False + + def can_be_downgraded(self): + return False + + def get_type(self): + return 'web' + + def get_type_icon_path(self) -> str: + return self.get_default_icon_path() + + def get_default_icon_path(self) -> str: + return resource.get_path('img/web.png', ROOT_DIR) + + def is_application(self): + return True + + def supports_disk_cache(self): + return self.installed and self.is_application() + + def get_disk_cache_path(self): + return CACHE_PATH + '/' + self.get_type() + + def get_data_to_cache(self) -> dict: + """ + :return: the application data that should be cached in disk / memory for quick access + """ + pass + + def fill_cached_data(self, data: dict): + pass + + def can_be_run(self) -> bool: + return self.installed + + def is_trustable(self) -> bool: + return False + + def get_publisher(self) -> str: + pass + + def has_screenshots(self) -> bool: + return False diff --git a/bauh/gems/web/npm.py b/bauh/gems/web/npm.py new file mode 100644 index 00000000..9721b78b --- /dev/null +++ b/bauh/gems/web/npm.py @@ -0,0 +1,6 @@ +from bauh.commons.system import run_cmd + + +def is_available() -> bool: + res = run_cmd('which npm', print_error=False) + return res and not res.strip().startswith('which ') diff --git a/bauh/gems/web/resources/img/web.png b/bauh/gems/web/resources/img/web.png new file mode 100644 index 00000000..2e6fb51c Binary files /dev/null and b/bauh/gems/web/resources/img/web.png differ diff --git a/bauh/gems/web/resources/locale/en b/bauh/gems/web/resources/locale/en new file mode 100644 index 00000000..990b3f71 --- /dev/null +++ b/bauh/gems/web/resources/locale/en @@ -0,0 +1 @@ +gem.web.info=It allows to install Web applications on your system \ No newline at end of file diff --git a/bauh/gems/web/resources/locale/pt b/bauh/gems/web/resources/locale/pt new file mode 100644 index 00000000..0a595ef1 --- /dev/null +++ b/bauh/gems/web/resources/locale/pt @@ -0,0 +1 @@ +gem.web.info=Permite instalar aplicações Web no seu sistema \ No newline at end of file diff --git a/bauh/view/core/controller.py b/bauh/view/core/controller.py index e0f65f4a..39d1b6f1 100755 --- a/bauh/view/core/controller.py +++ b/bauh/view/core/controller.py @@ -1,3 +1,4 @@ +import re import time import traceback from argparse import Namespace @@ -11,6 +12,8 @@ from bauh.api.abstract.model import SoftwarePackage, PackageUpdate, PackageHisto from bauh.api.exception import NoInternetException from bauh.commons import internet +RE_IS_URL = re.compile(r'^https?://.+') + SUGGESTIONS_LIMIT = 5 @@ -79,17 +82,17 @@ class GenericSoftwareManager(SoftwareManager): return available - def _search(self, word: str, man: SoftwareManager, disk_loader, res: SearchResult): + def _search(self, word: str, is_url: bool, man: SoftwareManager, disk_loader, res: SearchResult): if self._can_work(man): mti = time.time() - apps_found = man.search(words=word, disk_loader=disk_loader) + apps_found = man.search(words=word, disk_loader=disk_loader, is_url=is_url) mtf = time.time() self.logger.info(man.__class__.__name__ + " took {0:.2f} seconds".format(mtf - mti)) res.installed.extend(apps_found.installed) res.new.extend(apps_found.new) - def search(self, word: str, disk_loader: DiskCacheLoader = None, limit: int = -1) -> SearchResult: + def search(self, word: str, disk_loader: DiskCacheLoader = None, limit: int = -1, is_url: bool = False) -> SearchResult: ti = time.time() self._wait_to_be_ready() @@ -97,13 +100,15 @@ class GenericSoftwareManager(SoftwareManager): if internet.is_available(self.context.http_client, self.context.logger): norm_word = word.strip().lower() + + url_words = RE_IS_URL.match(norm_word) disk_loader = self.disk_loader_factory.new() disk_loader.start() threads = [] for man in self.managers: - t = Thread(target=self._search, args=(norm_word, man, disk_loader, res)) + t = Thread(target=self._search, args=(norm_word, url_words, man, disk_loader, res)) t.start() threads.append(t) diff --git a/bauh/view/qt/window.py b/bauh/view/qt/window.py index c7a449d2..3117d887 100755 --- a/bauh/view/qt/window.py +++ b/bauh/view/qt/window.py @@ -106,7 +106,6 @@ class ManageWindow(QWidget): self.toolbar_search.addWidget(label_pre_search) self.input_search = QLineEdit() - self.input_search.setMaxLength(20) self.input_search.setFrame(False) self.input_search.setPlaceholderText(self.i18n['window_manage.input_search.placeholder'] + "...") self.input_search.setToolTip(self.i18n['window_manage.input_search.tooltip'])