diff --git a/CHANGELOG.md b/CHANGELOG.md index 05bbd277..f17c9148 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). -## [0.X.X] +## [0.10.0] +### Features +- Debian + - allowing Debian packages to be managed + - required dependencies: **aptitude** + - basic actions supported: **search**, **install**, **uninstall**, **upgrade** + - custom actions supported: + - **synchronize packages**: synchronize the available packages on the repository (`aptitude update`) + - **index applications**: maps runnable installed packages (automatically done during initialization) + - **software sources**: launches the application responsible for managing software sources (at the moment only `software-properties-gtk` is supported) + - custom package actions supported: + - **purge**: removes the packages and all related configuration files ### Improvements - General diff --git a/README.md b/README.md index a3949e61..3900e723 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![GitHub release](https://img.shields.io/github/release/vinifmor/bauh.svg?label=Release)](https://github.com/vinifmor/bauh/releases/) [![PyPI](https://img.shields.io/pypi/v/bauh?label=PyPI)](https://pypi.org/project/bauh) [![AUR](https://img.shields.io/aur/version/bauh?label=AUR)](https://aur.archlinux.org/packages/bauh) [![AUR-staging](https://img.shields.io/aur/version/bauh-staging?label=AUR-staging)](https://aur.archlinux.org/packages/bauh-staging) [![License](https://img.shields.io/github/license/vinifmor/bauh?label=License)](https://github.com/vinifmor/bauh/blob/master/LICENSE) [![kofi](https://img.shields.io/badge/Ko--Fi-Donate-orange?style=flat&logo=ko-fi)](https://ko-fi.com/vinifmor) [![Follow on Twitter](https://img.shields.io/twitter/follow/bauh4linux?style=social&label=Twitter)](https://twitter.com/bauh4linux) -**bauh** (ba-oo), formerly known as **fpakman**, is a graphical interface for managing your Linux software (packages/applications). It currently supports the following formats: AppImage, ArchLinux repositories/AUR, Flatpak, Snap and Web applications. +**bauh** (ba-oo), formerly known as **fpakman**, is a graphical interface for managing your Linux software (packages/applications). It currently supports the following formats: AppImage, ArchLinux repositories/AUR, Debian packages, Flatpak, Snap and Web applications. Key features - A management panel where you can: search, install, uninstall, upgrade, downgrade and launch you applications (and more...) @@ -26,6 +26,7 @@ Key features 6. [Supported types](#types) - [AppImage](#type_appimage) - [Arch packages/AUR](#type_arch) + - [Debian](#type_deb) - [Flatpak](#type_flatpak) - [Snap](#type_snap) - [Native Web applications](#type_web) @@ -69,6 +70,7 @@ Key features ##### Optional dependencies (they should be installed with apt-get/apt) +- `aptitude`: Debian package management - `timeshift`: system backup - `aria2`: multi-threaded downloads - `axel`: multi-threaded downloads alternative @@ -285,6 +287,22 @@ aur_rebuild_detector: true # it checks if packages built with old library versio prefer_repository_provider: true # when there is just one repository provider for a given a dependency and several from AUR, it will be automatically picked. ``` +##### Debian packages +- Basic actions supported: **search**, **install**, **uninstall**, **upgrade** +- Custom actions supported: + - **synchronize packages**: synchronize the available packages on the repository (`aptitude update`) + - **index applications**: maps runnable installed packages (automatically done during initialization) + - **software sources**: launches the application responsible for managing software sources (at the moment only `software-properties-gtk` is supported) +- Custom package actions supported: + - **purge**: removes the packages and all related configuration files +- Files: + - runnable applications index: `~/.cache/bauh/debian/apps_idx.json` (or `/var/cache/bauh/debian/apps_idx.json` for **root**) + - package suggestions: `~/.cache/bauh/debian/suggestions.txt` (or `/var/cache/bauh/debian/suggestions.txt` for **root**) + - configuration: `~/.config/bauh/debian.yml` or `/etc/bauh/debian.yml` + - `index_apps.exp`: time period (**in minutes**) in which the installed applications cache is considered up-to-date during startup (default: `1440` -> 24 hours) + - `sync_pkgs.time`: time period (**in minutes**) in which the packages synchronization must be done on startup (default: `1440` -> 24 hours) + - `suggestions.exp`: it defines the period (**in hours**) in which the suggestions stored in disc will be considered up to date. Use 0 if you always want to update them. + - `pkg_sources.app`: it defines the application for managing the software sources. A `null` value detects one of the supported applications automatically. ##### Flatpak diff --git a/bauh/__init__.py b/bauh/__init__.py index 4d04c6b9..f9b95658 100644 --- a/bauh/__init__.py +++ b/bauh/__init__.py @@ -1,4 +1,4 @@ -__version__ = '0.9.29' +__version__ = '0.10.0' __app_name__ = 'bauh' import os diff --git a/bauh/api/abstract/context.py b/bauh/api/abstract/context.py index d327eed2..da729d48 100644 --- a/bauh/api/abstract/context.py +++ b/bauh/api/abstract/context.py @@ -14,7 +14,7 @@ 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, - internet_checker: InternetChecker, root_user: bool): + internet_checker: InternetChecker, root_user: bool, screen_width: int = -1, screen_height: int = -1): """ :param download_icons: if packages icons should be downloaded :param http_client: a shared instance of http client @@ -27,6 +27,8 @@ class ApplicationContext: :param distro :param app_name :param internet_checker + :param screen_width + :param screen_height """ self.download_icons = download_icons self.http_client = http_client @@ -44,6 +46,8 @@ class ApplicationContext: self.root_user = root_user self.root_password = None self.internet_checker = internet_checker + self.screen_width = screen_width + self.screen_height = screen_height def is_system_x86_64(self): return self.arch_x86_64 diff --git a/bauh/api/abstract/controller.py b/bauh/api/abstract/controller.py index e80f12b4..142d5bb4 100644 --- a/bauh/api/abstract/controller.py +++ b/bauh/api/abstract/controller.py @@ -4,7 +4,7 @@ import shutil from abc import ABC, abstractmethod from enum import Enum from pathlib import Path -from typing import List, Set, Type, Tuple, Optional, Generator +from typing import List, Set, Type, Tuple, Optional, Generator, TypeVar import yaml @@ -15,10 +15,12 @@ from bauh.api.abstract.model import SoftwarePackage, PackageUpdate, PackageHisto CustomSoftwareAction from bauh.api.abstract.view import ViewComponent +P = TypeVar('P', bound=SoftwarePackage) + class SearchResult: - def __init__(self, installed: Optional[List[SoftwarePackage]], new: Optional[List[SoftwarePackage]], total: int): + def __init__(self, installed: Optional[List[P]], new: Optional[List[P]], total: int): """ :param installed: already installed packages :param new: new packages found @@ -40,9 +42,19 @@ class SearchResult: self.total = total @classmethod - def empty(cls): + def empty(cls) -> "SearchResult": return cls(installed=[], new=[], total=0) + def __eq__(self, other): + if isinstance(other, SearchResult): + if self.installed == other.installed: + return self.new == other.new + + return False + + def __hash__(self): + return hash(self.installed) + hash(self.new) + class UpgradeRequirement: @@ -69,7 +81,7 @@ class UpgradeRequirement: class UpgradeRequirements: def __init__(self, to_install: Optional[List[UpgradeRequirement]], to_remove: Optional[List[UpgradeRequirement]], - to_upgrade: List[UpgradeRequirement], cannot_upgrade: Optional[List[UpgradeRequirement]]): + to_upgrade: Optional[List[UpgradeRequirement]], cannot_upgrade: Optional[List[UpgradeRequirement]]): """ :param to_install: additional packages that must be installed with the upgrade :param to_remove: non upgrading packages that should be removed due to conflicts with upgrading packages diff --git a/bauh/api/exception.py b/bauh/api/exception.py index 8d00ac1a..ae00707c 100644 --- a/bauh/api/exception.py +++ b/bauh/api/exception.py @@ -1,3 +1,6 @@ +from typing import Optional + class NoInternetException(Exception): pass + diff --git a/bauh/commons/singleton.py b/bauh/commons/singleton.py new file mode 100644 index 00000000..b502c17d --- /dev/null +++ b/bauh/commons/singleton.py @@ -0,0 +1,9 @@ +class Singleton(type): + + __instance = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls.__instance: + cls.__instance[cls] = super(Singleton, cls).__call__(*args, **kwargs) + + return cls.__instance[cls] diff --git a/bauh/commons/system.py b/bauh/commons/system.py index c63cb0a7..094b1910 100644 --- a/bauh/commons/system.py +++ b/bauh/commons/system.py @@ -4,7 +4,7 @@ import sys import time from io import StringIO from subprocess import PIPE -from typing import List, Tuple, Set, Dict, Optional +from typing import List, Tuple, Set, Dict, Optional, Iterable # default environment variables for subprocesses. from bauh.api.abstract.handler import ProcessWatcher @@ -26,7 +26,7 @@ USE_GLOBAL_INTERPRETER = bool(os.getenv('VIRTUAL_ENV')) def gen_env(global_interpreter: bool, lang: str = DEFAULT_LANG, extra_paths: Optional[Set[str]] = None) -> dict: custom_env = dict(os.environ) - if lang: + if lang is not None: custom_env['LANG'] = lang if global_interpreter: # to avoid subprocess calls to the virtualenv python interpreter instead of the global one. @@ -62,7 +62,7 @@ class SystemProcess: class SimpleProcess: - def __init__(self, cmd: List[str], cwd: str = '.', expected_code: int = 0, + def __init__(self, cmd: Iterable[str], cwd: str = '.', expected_code: int = 0, global_interpreter: bool = USE_GLOBAL_INTERPRETER, lang: str = DEFAULT_LANG, root_password: Optional[str] = None, extra_paths: Set[str] = None, error_phrases: Set[str] = None, wrong_error_phrases: Set[str] = None, shell: bool = False, success_phrases: Set[str] = None, extra_env: Optional[Dict[str, str]] = None, @@ -117,10 +117,13 @@ class ProcessHandler: def __init__(self, watcher: ProcessWatcher = None): self.watcher = watcher - def _notify_watcher(self, msg: str): + def _notify_watcher(self, msg: str, as_substatus: bool = False): if self.watcher: self.watcher.print(msg) + if as_substatus: + self.watcher.change_substatus(msg) + def handle(self, process: SystemProcess, error_output: StringIO = None, output_handler=None) -> bool: self._notify_watcher(' '.join(process.subproc.args) + '\n') @@ -181,7 +184,8 @@ class ProcessHandler: return process.subproc.returncode is None or process.subproc.returncode == 0 - def handle_simple(self, proc: SimpleProcess, output_handler=None, notify_watcher: bool = True) -> Tuple[bool, str]: + def handle_simple(self, proc: SimpleProcess, output_handler=None, notify_watcher: bool = True, + output_as_substatus: bool = False) -> Tuple[bool, str]: if notify_watcher: self._notify_watcher((proc.instance.args if isinstance(proc.instance.args, str) else ' '.join(proc.instance.args)) + '\n') @@ -205,7 +209,7 @@ class ProcessHandler: output_handler(line) if notify_watcher: - self._notify_watcher(line) + self._notify_watcher(line, as_substatus=output_as_substatus) proc.instance.wait() output.seek(0) diff --git a/bauh/commons/util.py b/bauh/commons/util.py index 01d2b962..8acd65c7 100644 --- a/bauh/commons/util.py +++ b/bauh/commons/util.py @@ -1,4 +1,21 @@ +import logging +from abc import ABC from datetime import datetime +from logging import Logger +from typing import Optional + + +class NullLoggerFactory(ABC): + + __instance: Optional[Logger] = None + + @classmethod + def logger(cls) -> Logger: + if cls.__instance is None: + cls.__instance = logging.getLogger('__null__') + cls.__instance.addHandler(logging.NullHandler()) + + return cls.__instance def deep_update(source: dict, overrides: dict): diff --git a/bauh/gems/appimage/resources/locale/ca b/bauh/gems/appimage/resources/locale/ca index 97cd839c..03dad8b6 100644 --- a/bauh/gems/appimage/resources/locale/ca +++ b/bauh/gems/appimage/resources/locale/ca @@ -44,7 +44,6 @@ appimage.update_database.uncompressing=Uncompressing files appimage.upgrade.failed=It was not possible to upgrade the following applications: {apps} appimage.task.db_update=Actualització de bases de dades appimage.task.db_update.checking=Checking for updates -appimage.task.suggestions=Downloading suggestions appimage.task.symlink_check=Checking symlinks appimage.uninstall.error.remove_folder=No s’ha pogut suprimir el directori d’instal·lació de l’aplicació {} appimage.warning.missing_db_files={appimage} database files were not found. Some actions will not work properly. diff --git a/bauh/gems/appimage/resources/locale/de b/bauh/gems/appimage/resources/locale/de index d91e2644..f61662d3 100644 --- a/bauh/gems/appimage/resources/locale/de +++ b/bauh/gems/appimage/resources/locale/de @@ -44,7 +44,6 @@ appimage.update_database.uncompressing=Uncompressing files appimage.upgrade.failed=It was not possible to upgrade the following applications: {apps} appimage.task.db_update=Updating databases appimage.task.db_update.checking=Checking for updates -appimage.task.suggestions=Downloading suggestions appimage.task.symlink_check=Checking symlinks appimage.uninstall.error.remove_folder=Anwendungordner {} konnte nicht entfernt werden appimage.warning.missing_db_files={appimage} database files were not found. Some actions will not work properly. diff --git a/bauh/gems/appimage/resources/locale/en b/bauh/gems/appimage/resources/locale/en index 14efc718..5a9fc536 100644 --- a/bauh/gems/appimage/resources/locale/en +++ b/bauh/gems/appimage/resources/locale/en @@ -44,7 +44,6 @@ appimage.update_database.uncompressing=Uncompressing files appimage.upgrade.failed=It was not possible to upgrade the following applications: {apps} appimage.task.db_update=Updating databases appimage.task.db_update.checking=Checking for updates -appimage.task.suggestions=Downloading suggestions appimage.task.symlink_check=Checking symlinks appimage.uninstall.error.remove_folder=Could not remove the application installation directory {} appimage.warning.missing_db_files={appimage} database files were not found. Some actions will not work properly. diff --git a/bauh/gems/appimage/resources/locale/es b/bauh/gems/appimage/resources/locale/es index 3b11a4b3..454b69d8 100644 --- a/bauh/gems/appimage/resources/locale/es +++ b/bauh/gems/appimage/resources/locale/es @@ -44,7 +44,6 @@ appimage.update_database.uncompressing=Descomprindo archivos appimage.upgrade.failed=No fue posible actualizar las siguientes aplicaciones: {apps} appimage.task.db_update=Actualizando base de datos appimage.task.db_update.checking=Buscando actualizaciones -appimage.task.suggestions=Descargando sugerencias appimage.task.symlink_check=Verificando links simbólicos appimage.uninstall.error.remove_folder=No se pudo eliminar el directorio de instalación de la aplicación {} appimage.warning.missing_db_files=No se encontraron archivos de la base de datos {appimage}. Algunas acciones no funcionarán correctamente. diff --git a/bauh/gems/appimage/resources/locale/fr b/bauh/gems/appimage/resources/locale/fr index 3c7b57e6..792d2682 100644 --- a/bauh/gems/appimage/resources/locale/fr +++ b/bauh/gems/appimage/resources/locale/fr @@ -45,7 +45,6 @@ appimage.update_database.uncompressing=Uncompressing files appimage.upgrade.failed=It was not possible to upgrade the following applications: {apps} appimage.task.db_update=Mise à jour des bases de données appimage.task.db_update.checking=Checking for updates -appimage.task.suggestions=Downloading suggestions appimage.task.symlink_check=Verification des symlinks appimage.uninstall.error.remove_folder=Impossible de supprimer le répertoire d'installation {} appimage.warning.missing_db_files={appimage} database files were not found. Some actions will not work properly. diff --git a/bauh/gems/appimage/resources/locale/it b/bauh/gems/appimage/resources/locale/it index a347d33e..cfd6b3c9 100644 --- a/bauh/gems/appimage/resources/locale/it +++ b/bauh/gems/appimage/resources/locale/it @@ -45,7 +45,6 @@ appimage.update_database.uncompressing=Uncompressing files appimage.upgrade.failed=It was not possible to upgrade the following applications: {apps} appimage.task.db_update=Aggiornamento dei database appimage.task.db_update.checking=Checking for updates -appimage.task.suggestions=Downloading suggestions appimage.task.symlink_check=Checking symlinks appimage.uninstall.error.remove_folder=Impossibile rimuovere la directory di installazione dell'applicazione {} appimage.warning.missing_db_files={appimage} database files were not found. Some actions will not work properly. diff --git a/bauh/gems/appimage/resources/locale/pt b/bauh/gems/appimage/resources/locale/pt index 0910990c..284dbb2f 100644 --- a/bauh/gems/appimage/resources/locale/pt +++ b/bauh/gems/appimage/resources/locale/pt @@ -1,7 +1,7 @@ appimage.config.database.expiration=Expiração do banco de dados appimage.config.database.expiration.tip=Define o período (em minutos) no qual o banco de dados será considerado atualizado durante o processo de inicialização. Use 0 se quiser que ele sempre seja atualizado. appimage.config.suggestions.expiration=Expiração das sugestões -appimage.config.suggestions.expiration.tip=Define o período (em horas) no qual as sugestões armazenadas em disco serão consideradas atualizadas. Use 0 se quiser que elas sempre seja atualizadas. +appimage.config.suggestions.expiration.tip=Define o período (em horas) no qual as sugestões armazenadas em disco serão consideradas atualizadas. Use 0 se quiser que elas sempre sejam atualizadas. appimage.custom_action.install_file=Instalar arquivo AppImage appimage.custom_action.install_file.desc=Instala um arquivo AppImage no sistema appimage.custom_action.install_file.details=Detalhes de instalação de AppImage @@ -44,7 +44,6 @@ appimage.update_database.uncompressing=Descomprimindo arquivos appimage.upgrade.failed=Não foi possível atualizar as seguintes aplicações: {apps} appimage.task.db_update=Atualizando base de dados appimage.task.db_update.checking=Checando atualizações -appimage.task.suggestions=Baixando sugestões appimage.task.symlink_check=Verificando links simbólicos appimage.uninstall.error.remove_folder=Não foi possível remover o diretóri ode instalação do aplicativo {} appimage.warning.missing_db_files=Arquivos do banco de dados {appimage} não foram encontrados. Algumas ações não funcionarão corretamente. diff --git a/bauh/gems/appimage/resources/locale/ru b/bauh/gems/appimage/resources/locale/ru index f7a9128f..3b37d2ab 100644 --- a/bauh/gems/appimage/resources/locale/ru +++ b/bauh/gems/appimage/resources/locale/ru @@ -44,7 +44,6 @@ appimage.update_database.uncompressing=Uncompressing files appimage.upgrade.failed=It was not possible to upgrade the following applications: {apps} appimage.task.db_update=Обновление базы данных appimage.task.db_update.checking=Checking for updates -appimage.task.suggestions=Downloading suggestions appimage.task.symlink_check=Checking symlinks appimage.uninstall.error.remove_folder=Не удалось удалить каталог приложения {} appimage.warning.missing_db_files={appimage} database files were not found. Some actions will not work properly. diff --git a/bauh/gems/appimage/resources/locale/tr b/bauh/gems/appimage/resources/locale/tr index 7cac510f..1b8f4f1c 100644 --- a/bauh/gems/appimage/resources/locale/tr +++ b/bauh/gems/appimage/resources/locale/tr @@ -44,7 +44,6 @@ appimage.update_database.uncompressing=Uncompressing files appimage.upgrade.failed=It was not possible to upgrade the following applications: {apps} appimage.task.db_update=Veritabanları güncelleniyor appimage.task.db_update.checking=Checking for updates -appimage.task.suggestions=Downloading suggestions appimage.task.symlink_check=Checking symlinks appimage.uninstall.error.remove_folder={} uygulama kurulum dizini kaldırılamadı appimage.warning.missing_db_files={appimage} database files were not found. Some actions will not work properly. diff --git a/bauh/gems/appimage/worker.py b/bauh/gems/appimage/worker.py index e416f5f9..ea6ede95 100644 --- a/bauh/gems/appimage/worker.py +++ b/bauh/gems/appimage/worker.py @@ -295,7 +295,7 @@ class AppImageSuggestionsDownloader(Thread): self.taskman = taskman self.config = appimage_config self.task_id = 'appim.suggestions' - self.taskman.register_task(id_=self.task_id, label=i18n['appimage.task.suggestions'], icon_path=get_icon_path()) + self.taskman.register_task(id_=self.task_id, label=i18n['task.download_suggestions'], icon_path=get_icon_path()) def should_download(self, appimage_config: dict) -> bool: try: diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index 06939bc0..693af00b 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -635,7 +635,7 @@ class ArchManager(SoftwareManager): if ignored: for p in pkgs: if p.name in ignored: - p.update_ignored = True + p.updates_ignored = True return SearchResult(pkgs, None, len(pkgs)) diff --git a/bauh/gems/debian/__init__.py b/bauh/gems/debian/__init__.py new file mode 100644 index 00000000..e4121fd3 --- /dev/null +++ b/bauh/gems/debian/__init__.py @@ -0,0 +1,12 @@ +import os + +from bauh.api.paths import CACHE_DIR, CONFIG_DIR +from bauh.commons import resource + +ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) + +DEBIAN_CACHE_DIR = f'{CACHE_DIR}/debian' +APP_INDEX_FILE = f'{DEBIAN_CACHE_DIR}/apps_idx.json' +CONFIG_FILE = f'{CONFIG_DIR}/debian.yml' +PACKAGE_SYNC_TIMESTAMP_FILE = f'{DEBIAN_CACHE_DIR}/sync_pkgs.ts' +DEBIAN_ICON_PATH = resource.get_path('img/debian.svg', ROOT_DIR) diff --git a/bauh/gems/debian/aptitude.py b/bauh/gems/debian/aptitude.py new file mode 100644 index 00000000..76b82cf8 --- /dev/null +++ b/bauh/gems/debian/aptitude.py @@ -0,0 +1,424 @@ +import re +import time +import traceback +from contextlib import contextmanager +from enum import Enum +from logging import Logger +from math import ceil +from queue import Queue +from threading import Thread +from typing import Iterable, Optional, Pattern, Dict, Set, Tuple, Generator, Collection + +from bauh.api.abstract.handler import ProcessWatcher +from bauh.commons import system +from bauh.commons.html import bold +from bauh.commons.system import SimpleProcess +from bauh.commons.util import size_to_byte +from bauh.gems.debian.common import strip_maintainer_email, strip_section +from bauh.gems.debian.model import DebianPackage, DebianTransaction +from bauh.view.util.translation import I18n + + +def map_package_name(string: str): + name_split = string.split(':') + + if len(name_split) > 2: + return ':'.join(name_split[0:-1]) + + return name_split[0] + + +class AptitudeAction(Enum): + INSTALL = 0 + UPGRADE = 1 + REMOVE = 3 + + +class Aptitude: + + def __init__(self, logger: Logger): + self._log = logger + self._re_show_attr: Optional[Pattern] = None + self._env: Optional[Dict[str, str]] = None + self._list_attrs: Optional[Set[str]] = None + self._re_to_install: Optional[Pattern] = None + self._re_transaction_pkg: Optional[Pattern] = None + self._size_attrs: Optional[Tuple[str]] = None + self._default_lang = '' + self._ignored_fields: Optional[Set[str]] = None + + def show(self, pkgs: Iterable[str], attrs: Optional[Collection[str]] = None, verbose: bool = False) \ + -> Optional[Dict[str, Dict[str, object]]]: + + if pkgs: + force_verbose = verbose if verbose else ('compressed size' in attrs if attrs else False) + code, output = system.execute(f"aptitude show -q {' '.join(pkgs)}{' -v' if force_verbose else ''}", + shell=True, custom_env=self.env) + + if code == 0 and output: + info, pkg = dict(), None + for field, val in self.re_show_attr.findall('\n' + output): + final_field, final_val = field.strip().lower(), val.strip() + + if final_field == 'package': + pkg = {} + info[final_val] = pkg + elif final_field not in self.ignored_fields and (not attrs or final_field in attrs): + if final_val: + if final_field in self.list_attrs: + final_val = tuple((v.strip() for v in final_val.split(',') if v)) + elif final_field in self.size_attrs: + size_split = final_val.split(' ') + + if len(size_split) >= 1: + number = float(size_split[0].replace(',', '.')) + measure = size_split[1].strip().lower() if len(size_split) >= 2 else 'b' + final_val = size_to_byte(number, measure) + else: + self._log.warning(f"Unhandled value ({val}) for attribute '{field}'") + final_val = None + + pkg[final_field] = final_val + + return info + + def simulate_removal(self, packages: Iterable[str], purge: bool = False) -> Optional[DebianTransaction]: + code, output = system.execute(self.gen_remove_cmd(packages, purge, simulate=True), shell=True, + custom_env=self.env) + + if code == 0 and output: + return self.map_transaction_output(output) + + def map_transaction_output(self, output: str) -> DebianTransaction: + to_install, to_upgrade, to_remove = None, None, None + current_collection = None + + for line in output.split('\n'): + if line.startswith('The following NEW packages will be installed:'): + to_install = set() + current_collection = to_install + elif line.startswith('The following packages will be upgraded:'): + to_upgrade = set() + current_collection = to_upgrade + elif line.startswith('The following packages will be REMOVED:'): + to_remove = set() + current_collection = to_remove + elif line.startswith('Would download/install/remove packages'): + break + elif current_collection is not None and line.startswith(' '): + for n, _, v, __, lv, ___, size in self.re_transaction_pkg.findall(line): + pkg = DebianPackage(name=n, version=v, latest_version=lv if lv else v, transaction_size=0) + + if size: + size_split = size.strip().split(' ') + + try: + number = float(size_split[0].strip().replace(',', '.')) + measure = size_split[1].strip()[0].lower() if len(size_split) >= 2 else 'b' + pkg.transaction_size = size_to_byte(number, measure) + except: + traceback.print_exc() + pass + + current_collection.add(pkg) + + return DebianTransaction(to_install=tuple(to_install) if to_install else tuple(), + to_remove=tuple(to_remove) if to_remove else tuple(), + to_upgrade=tuple(to_upgrade) if to_upgrade else tuple()) + + def simulate_upgrade(self, packages: Iterable[str]) -> DebianTransaction: + code, output = system.execute(self.gen_transaction_cmd('upgrade', packages, simulate=True), + shell=True, custom_env=self.env) + + if code == 0 and output: + return self.map_transaction_output(output) + + def upgrade(self, packages: Iterable[str], root_password: Optional[str]) -> SimpleProcess: + cmd = self.gen_transaction_cmd('upgrade', packages).split(' ') + return SimpleProcess(cmd=cmd, shell=True, lang=self._default_lang, + root_password=root_password) + + def update(self, root_password: Optional[str]) -> SimpleProcess: + return SimpleProcess(('aptitude', 'update'), root_password=root_password, shell=True, lang=self._default_lang) + + def simulate_installation(self, packages: Iterable[str]) -> Optional[DebianTransaction]: + code, output = system.execute(self.gen_transaction_cmd('install', packages, simulate=True), + shell=True, custom_env=self.env) + + if code == 0 and output: + return self.map_transaction_output(output) + + def install(self, packages: Iterable[str], root_password: Optional[str]) -> SimpleProcess: + cmd = self.gen_transaction_cmd('install', packages).split(' ') + return SimpleProcess(cmd=cmd, shell=True, lang=self._default_lang, root_password=root_password) + + def read_installed(self) -> Generator[DebianPackage, None, None]: + yield from self.search(query='~i') + + def read_updates(self) -> Generator[Tuple[str, str], None, None]: + _, output = system.execute(f"aptitude search ~U -q -F '%p^%V' --disable-columns --no-gui", + shell=True, + custom_env=self.env) + + if output: + for line in output.split('\n'): + line_split = line.strip().split('^') + + if len(line_split) == 2 and line_split[1] != '': + yield line_split[0], line_split[1] + + def search(self, query: str, fill_size: bool = False) -> Generator[DebianPackage, None, None]: + attrs = f"%p^%v^%V^%m^%s^{'%I^' if fill_size else ''}%d" + _, output = system.execute(f"aptitude search {query} -q -F '{attrs}' --disable-columns", + shell=True, + custom_env=self.env) + + if output: + no_attrs = 7 if fill_size else 6 + + for line in output.split('\n'): + line_split = line.strip().split('^', maxsplit=no_attrs - 1) + + if len(line_split) == no_attrs: + latest_version = line_split[2] if line_split[2] != '' else None + + size = None + + if fill_size: + size_split = line_split[no_attrs - 2].split(' ') + number = float(size_split[0].replace(',', '.')) + measure = size_split[1][0].lower() if len(size_split) >= 2 else 'b' + + try: + size = size_to_byte(number, measure) + except: + traceback.print_exc() + + if latest_version is not None: + installed_version = line_split[1] if line_split[1] != '' else None + section = strip_section(line_split[4]) + + yield DebianPackage(name=line_split[0], + version=installed_version if installed_version else latest_version, + latest_version=latest_version, + installed=bool(installed_version), + update=installed_version is not None and installed_version != latest_version, + maintainer=strip_maintainer_email(line_split[3]), + categories=(section,) if section else None, + uncompressed_size=size, + description=line_split[no_attrs - 1]) + + def search_by_name(self, names: Iterable[str], fill_size: bool = False) -> Generator[DebianPackage, None, None]: + query = f"'({'|'.join(f'?exact-name({n})' for n in names)})'" + yield from self.search(query=query, fill_size=fill_size) + + def remove(self, packages: Iterable[str], root_password: Optional[str], purge: bool = False) -> SimpleProcess: + return SimpleProcess(cmd=self.gen_remove_cmd(packages, purge).split(' '), shell=True, + lang=self._default_lang, root_password=root_password) + + def read_installed_names(self) -> Generator[str, None, None]: + code, output = system.execute("aptitude search ~i -q -F '%p' --disable-columns", + shell=True, + custom_env=self.env) + + if output: + for line in output.split('\n'): + if line: + yield line + + @property + def re_show_attr(self) -> Pattern: + if self._re_show_attr is None: + self._re_show_attr = re.compile(r'(\n\w+[a-zA-Z0-9\-\s]*):\s+(.+)') + + return self._re_show_attr + + @property + def env(self) -> Dict[str, str]: + if self._env is None: + self._env = system.gen_env(global_interpreter=system.USE_GLOBAL_INTERPRETER, + lang=self._default_lang) + + return self._env + + @property + def list_attrs(self) -> Set[str]: + if self._list_attrs is None: + self._list_attrs = {'depends', 'provides', 'replaces', 'recommends', 'suggests', 'conflicts', + 'state', 'predepends', 'breaks'} + + return self._list_attrs + + @property + def re_transaction_pkg(self) -> Pattern: + if self._re_transaction_pkg is None: + self._re_transaction_pkg = re.compile(r'([a-zA-Z0-9\-_@~.+]+)({\w+})?\s*\[([a-zA-Z0-9\-_@~.+:]+)' + r'(\s+->\s+([a-zA-Z0-9\-_@~.+:]+))?](\s*<([\-+]?[0-9.,]+\s+\w+)>)?') + + return self._re_transaction_pkg + + @property + def size_attrs(self) -> Tuple[str]: + if self._size_attrs is None: + self._size_attrs = {'compressed size', 'uncompressed size'} + + return self._size_attrs + + @property + def ignored_fields(self) -> Set[str]: + if self._ignored_fields is None: + self._ignored_fields = {'sha1', 'sha256', 'sha512', 'checksum-filesize'} + + return self._ignored_fields + + @classmethod + def gen_remove_cmd(cls, packages: Iterable[str], purge: bool, simulate: bool = False) -> str: + return cls.gen_transaction_cmd('purge' if purge else 'remove', packages, simulate) + + @staticmethod + def gen_transaction_cmd(type_: str, packages: Iterable[str], simulate: bool = False) -> str: + return f"aptitude {type_} -q -y --no-gui --full-resolver {' '.join(packages)}" \ + f" -o Aptitude::ProblemResolver::RemoveScore=9999999" \ + f" -o Aptitude::ProblemResolver::EssentialRemoveScore=9999999" \ + f"{' -V -s -Z' if simulate else ''}" + + +class AptitudeOutputHandler(Thread): + + def __init__(self, i18n: I18n, targets: Iterable[str], re_download: Pattern, watcher: ProcessWatcher, + action: AptitudeAction): + super(AptitudeOutputHandler, self).__init__() + self._i18n = i18n + self._re_download = re_download + self._watcher = watcher + self._targets = set(targets) if targets is not None else None + self._unpacking = 0 + self._removing = 0 + self._downloading = 0 + self._to_process = Queue() + self._work = True + self._action = action + + def stop_working(self): + self._work = False + + def handle(self, string: str): + self._to_process.put(string) + + @property + def total_targets(self) -> int: + return len(self._targets) if self._targets else 0 + + @property + def processed(self) -> int: + return self._removing if self._action == AptitudeAction.REMOVE else self._unpacking + + def _get_progress(self, current: int) -> str: + if self.total_targets > 0: + if self._action == AptitudeAction.REMOVE: + total = self._removing + else: + if self.total_targets == self._unpacking: + total = self.total_targets + else: + total = ceil((self._unpacking + self._downloading) / 2) + + return f'({total / self.total_targets * 100:.2f}%) [{current}/{self.total_targets}] ' + + return '' + + def run(self): + while self._work: + time.sleep(0.001) + + if self._to_process.empty(): + continue + + string = self._to_process.get() + + if self.total_targets > 0 and self.total_targets == self._unpacking: + self._watcher.change_substatus(self._i18n['debian.output.finishing']) + continue + + if string: + if self._action != AptitudeAction.REMOVE and string.startswith('Unpacking '): + unpacking = string.split(' ') + + if len(unpacking) >= 2 and unpacking[1]: + pkg = map_package_name(unpacking[1].strip()) + + if self._targets and pkg in self._targets: + self._unpacking += 1 + + msg = f"{self._get_progress(self._unpacking)}" \ + f"{self._i18n['debian.output.unpacking'].format(pkg=bold(pkg))}" + + self._watcher.change_substatus(msg) + + continue + + if self._action == AptitudeAction.REMOVE and string.startswith('Removing '): + unpacking = string.split(' ') + + if len(unpacking) >= 2 and unpacking[1]: + pkg = unpacking[1].strip() + + if self._targets and pkg in self._targets: + self._removing += 1 + + msg = f"{self._get_progress(self._removing)}" \ + f"{self._i18n['debian.output.removing'].format(pkg=bold(pkg))}" + + self._watcher.change_substatus(msg) + + continue + + download = self._re_download.findall(string) + + if download: + data = download[0].split(' ') + + if len(data) >= 4: + pkg = data[3].strip() + + if self._targets and pkg in self._targets: + self._downloading += 1 + + msg = f"{self._get_progress(self._downloading)}" \ + f"{self._i18n['debian.output.downloading'].format(pkg=bold(pkg))}" + + self._watcher.change_substatus(msg) + + continue + + _processed = self.processed + if self._targets and _processed > 0: + self._watcher.change_substatus(self._get_progress(_processed).strip()) + continue + + self._watcher.change_substatus(' ') + + +class AptitudeOutputHandlerFactory: + + def __init__(self, i18n: I18n): + self._i18n = i18n + self._re_download: Optional[Pattern] = None + + @property + def re_download(self) -> Pattern: + if self._re_download is None: + self._re_download = re.compile(r'Get:\s+\d+\s+https?://(.+)') + + return self._re_download + + @contextmanager + def start(self, watcher: ProcessWatcher, targets: Iterable[str], action: AptitudeAction): + handler = AptitudeOutputHandler(i18n=self._i18n, re_download=self.re_download, + watcher=watcher, targets=targets, action=action) + handler.start() + + yield handler.handle + + handler.stop_working() + handler.join() diff --git a/bauh/gems/debian/common.py b/bauh/gems/debian/common.py new file mode 100644 index 00000000..910525ea --- /dev/null +++ b/bauh/gems/debian/common.py @@ -0,0 +1,32 @@ +from typing import Dict, Optional + +from bauh.gems.debian.model import DebianPackage + + +def strip_maintainer_email(maintainer: str) -> str: + return maintainer.split('<')[0].strip() + + +def strip_section(section: str) -> Optional[str]: + if section: + section_split = section.split('/') + return section_split[1] if len(section_split) > 1 else section + + +def fill_show_data(pkg: DebianPackage, data: Dict[str, object]): + if data: + for attr, val in data.items(): + final_attr = attr.replace(' ', '_') + + if not val or val == '': + final_val = None + else: + if attr == 'maintainer': + final_val = strip_maintainer_email(str(val)) + elif attr == 'section': + final_attr = 'categories' + final_val = (strip_section(str(val)),) + else: + final_val = val + + setattr(pkg, final_attr, final_val) diff --git a/bauh/gems/debian/config.py b/bauh/gems/debian/config.py new file mode 100644 index 00000000..c2307ca1 --- /dev/null +++ b/bauh/gems/debian/config.py @@ -0,0 +1,16 @@ +from bauh.commons.config import YAMLConfigManager +from bauh.gems.debian import CONFIG_FILE + + +class DebianConfigManager(YAMLConfigManager): + + def __init__(self): + super(DebianConfigManager, self).__init__(config_file_path=CONFIG_FILE) + + def get_default_config(self) -> dict: + return { + 'suggestions.exp': 24, # hours + 'index_apps.exp': 1440, # 24 hours + 'sync_pkgs.time': 1440, # 24 hours + 'pkg_sources.app': None + } diff --git a/bauh/gems/debian/controller.py b/bauh/gems/debian/controller.py new file mode 100644 index 00000000..c2b5ded4 --- /dev/null +++ b/bauh/gems/debian/controller.py @@ -0,0 +1,857 @@ +import os.path +import shutil +import traceback +from operator import attrgetter +from pathlib import Path +from shutil import which +from subprocess import Popen +from threading import Thread +from typing import List, Optional, Tuple, Set, Type, Dict, Iterable, Generator + +from bauh.api.abstract.context import ApplicationContext +from bauh.api.abstract.controller import SoftwareManager, SoftwareAction, TransactionResult, UpgradeRequirements, \ + SearchResult, UpgradeRequirement +from bauh.api.abstract.disk import DiskCacheLoader +from bauh.api.abstract.handler import TaskManager, ProcessWatcher +from bauh.api.abstract.model import SoftwarePackage, PackageSuggestion, PackageUpdate, PackageHistory, \ + CustomSoftwareAction +from bauh.api.abstract.view import ViewComponent, TextInputComponent, PanelComponent, FormComponent, MessageType, \ + SingleSelectComponent, InputOption, SelectViewType +from bauh.api.paths import CONFIG_DIR +from bauh.commons.html import bold +from bauh.commons.system import get_human_size_str, ProcessHandler +from bauh.commons.util import NullLoggerFactory +from bauh.gems.debian import DEBIAN_ICON_PATH +from bauh.gems.debian.aptitude import Aptitude, AptitudeOutputHandlerFactory, AptitudeAction +from bauh.gems.debian.common import fill_show_data +from bauh.gems.debian.config import DebianConfigManager +from bauh.gems.debian.gui import DebianViewBridge +from bauh.gems.debian.index import ApplicationIndexer, ApplicationIndexError, ApplicationsMapper +from bauh.gems.debian.model import DebianPackage, DebianApplication +from bauh.gems.debian.suggestions import DebianSuggestionsDownloader +from bauh.gems.debian.tasks import UpdateApplicationIndex, MapApplications, SynchronizePackages + + +class DebianPackageManager(SoftwareManager): + + def __init__(self, context: ApplicationContext): + super(DebianPackageManager, self).__init__(context) + self._i18n = context.i18n + self._log = context.logger + self._types: Optional[Set[Type[SoftwarePackage]]] = None + self._enabled = True + self._app_indexer: Optional[ApplicationIndexer] = None + self._apps_index: Optional[Dict[str, DebianApplication]] = None + self._configman: Optional[DebianConfigManager] = None + self._action_launch_sources: Optional[CustomSoftwareAction] = None + self._default_actions: Optional[Iterable[CustomSoftwareAction]] = None + self._view: Optional[DebianViewBridge] = None + self._app_mapper: Optional[ApplicationsMapper] = None + self._aptitude: Optional[Aptitude] = None + self._output_handler: Optional[AptitudeOutputHandlerFactory] = None + self._known_sources_apps: Optional[Tuple[str, ...]] = None + self._install_show_attrs: Optional[Set[str]] = None + self._file_ignored_updates: Optional[str] = None + self._suggestions_downloader: Optional[DebianSuggestionsDownloader] = None + + def _update_apps_index(self, apps: Iterable[DebianApplication]): + self._apps_index = {app.name: app for app in apps} if apps else dict() + + def search(self, words: str, disk_loader: Optional[DiskCacheLoader], limit: int, is_url: bool) -> SearchResult: + res = SearchResult.empty() + + if not is_url: + for pkg in self.aptitude.search(words): + if pkg.installed: + pkg.bind_app(self.apps_index.get(pkg.name)) + res.installed.append(pkg) + else: + res.new.append(pkg) + + return res + + def _fill_ignored_updates(self, output: Set[str]): + + try: + with open(self.file_ignored_updates) as f: + ignored_str = f.read() + except FileNotFoundError: + return + + if ignored_str: + for line in ignored_str.split('\n'): + line_clean = line.strip() + + if line_clean: + output.add(line_clean) + + def read_installed(self, disk_loader: Optional[DiskCacheLoader], pkg_types: Optional[Set[Type[SoftwarePackage]]], + internet_available: bool, limit: int = -1, only_apps: bool = False, + names: Optional[Iterable[str]] = None) -> SearchResult: + + ignored_updates = set() + fill_ignored_updates = Thread(target=self._fill_ignored_updates, args=(ignored_updates,)) + fill_ignored_updates.start() + + res = SearchResult(installed=[], new=None, total=0) + + for pkg in self.aptitude.read_installed(): + if fill_ignored_updates.is_alive(): + fill_ignored_updates.join() + + pkg.bind_app(self.apps_index.get(pkg.name)) + pkg.updates_ignored = bool(ignored_updates and pkg.name in ignored_updates) + res.installed.append(pkg) + + return res + + def downgrade(self, pkg: SoftwarePackage, root_password: str, handler: ProcessWatcher) -> bool: + return False + + def upgrade(self, requirements: UpgradeRequirements, root_password: str, watcher: ProcessWatcher) -> bool: + handler = ProcessHandler(watcher) + + targets = (r.pkg.name for r in (*requirements.to_upgrade, *(requirements.to_install or ()))) + + with self.output_handler.start(watcher=watcher, targets=targets, action=AptitudeAction.UPGRADE) as handle: + to_upgrade = (r.pkg.name for r in requirements.to_upgrade) + success, _ = handler.handle_simple(self.aptitude.upgrade(packages=to_upgrade, + root_password=root_password), + output_handler=handle) + return success + + def _fill_updates(self, output: Dict[str, str]): + for name, version in self.aptitude.read_updates(): + output[name] = version + + def uninstall(self, pkg: DebianPackage, root_password: str, watcher: ProcessWatcher, + disk_loader: Optional[DiskCacheLoader], purge: bool = False) -> TransactionResult: + + watcher.change_substatus(self._i18n['debian.simulate_operation']) + + transaction = self.aptitude.simulate_removal((pkg.name,), purge=purge) + + if not transaction or not transaction.to_remove: + return TransactionResult.fail() + + if pkg not in transaction.to_remove: + watcher.show_message(title=self._i18n['popup.title.error'], + body=self._i18n['debian.remove.impossible'].format(pkg=bold(pkg.name)), + type_=MessageType.ERROR) + return TransactionResult.fail() + + watcher.change_substatus('') + + deps = tuple(p for p in transaction.to_remove if p.name != pkg.name) + + if deps: + # updates are required to be filled in case the dependencies are currently displayed on the view + updates = dict() + fill_updates = Thread(target=self._fill_updates, args=(updates,)) + fill_updates.start() + + deps_data = self.aptitude.show((p.name for p in deps), attrs=('description', 'maintainer', 'section')) + + if deps_data: + for p in deps: + fill_show_data(p, deps_data.get(p.name)) + + if not self.view.confirm_removal(source_pkg=pkg.name, dependencies=deps, watcher=watcher): + return TransactionResult.fail() + + fill_updates.join() + + if updates: + for p in deps: + latest_version = updates.get(p.name) + + if latest_version is not None and p.version != latest_version: + p.latest_version = latest_version + p.update = True + + watcher.change_substatus(self._i18n['debian.uninstall.removing']) + + handler = ProcessHandler(watcher) + to_remove = tuple(p.name for p in transaction.to_remove) + with self.output_handler.start(watcher=watcher, targets=to_remove, action=AptitudeAction.REMOVE) as handle: + removed, _ = handler.handle_simple(self.aptitude.remove(packages=to_remove, root_password=root_password, + purge=purge), + output_handler=handle) + + if not removed: + return TransactionResult.fail() + + watcher.change_substatus(self._i18n['debian.uninstall.validating']) + + current_installed_names = set(self.aptitude.read_installed_names()) + + watcher.change_substatus('') + + all_removed, apps_removed, not_removed_names = [], set(), set() + + for p in transaction.to_remove: + if p.name not in current_installed_names: + instance = p if p != pkg else pkg + + all_removed.append(instance) + + if instance.app: + apps_removed.add(instance.app) + + instance.installed = False + instance.version = instance.latest_version + instance.update = False + instance.bind_app(None) + else: + not_removed_names.add(p.name) + + if apps_removed: # updating apps index + watcher.print(self._i18n['debian.app_index.updating'] + ' ...') + watcher.change_substatus(self._i18n['debian.app_index.updating']) + indexed_apps = set(self.app_indexer.read_index()) + + if indexed_apps: + new_index = indexed_apps.difference(apps_removed) + try: + self.app_indexer.update_index(new_index, update_timestamp=False) + self._update_apps_index(new_index) + self._log.info(f"Debian applications removed from the index: " + f"{', '.join((a.name for a in apps_removed))}") + except ApplicationIndexError: + pass + + watcher.change_substatus('') + + success = True + if not_removed_names: + success = pkg.name not in not_removed_names + not_removed_str = ', '.join((bold(p) for p in sorted(not_removed_names))) + watcher.show_message(title=self._i18n[f"popup.title.{'warning' if success else 'error'}"], + body=self._i18n['debian.uninstall.failed_to_remove'].format(no=len(not_removed_names), + pkgs=not_removed_str), + type_=MessageType.WARNING if success else MessageType.ERROR) + + return TransactionResult(success=success, installed=None, removed=all_removed) + + def _map_dependents(self, packages_data: Dict[str, Dict[str, object]]) -> Optional[Dict[str, Set[str]]]: + if packages_data: + dependents = None + if packages_data: + dependents = dict() + + for p, data in packages_data.items(): + for attr in ('depends', 'predepends'): + dep_exps = data.get(attr) + + if isinstance(dep_exps, tuple): + for exp in dep_exps: + dep = exp.split(' ')[0].strip() + dep_dependents = dependents.get(dep) + if dep_dependents is None: + dep_dependents = set() + dependents[dep] = dep_dependents + + dep_dependents.add(p) + + return dependents + + def get_upgrade_requirements(self, pkgs: List[DebianPackage], root_password: str, watcher: ProcessWatcher) \ + -> UpgradeRequirements: + + transaction = self.aptitude.simulate_upgrade((p.name for p in pkgs)) + + if transaction: + size_to_query = (f'{p.name}={p.latest_version}' for p in (*transaction.to_upgrade, + *transaction.to_install, + *transaction.to_remove)) + + update_extra_attrs = ('compressed size', *(('depends', 'predepends') if transaction.to_install else ())) + update_data = self.aptitude.show(pkgs=size_to_query, attrs=update_extra_attrs) + + to_install = None + if transaction.to_install: + dependents = self._map_dependents(update_data) + + to_install = [] + for p in transaction.to_install: + p_data = update_data.get(p.name) if update_data else None + req_size = p_data.get('compressed size') if p_data else None + + reason = None + if dependents: + p_dependents = dependents.get(p.name) + + if p_dependents: + deps_str = ', '.join((bold(d) for d in p_dependents)) + reason = self._i18n['debian.transaction.dependency_of'].format(pkgs=deps_str) + + to_install.append(UpgradeRequirement(pkg=p, reason=reason, required_size=req_size, + extra_size=p.transaction_size)) + + to_install.sort(key=attrgetter('pkg.name')) + to_install = tuple(to_install) + + to_remove = None + if transaction.to_remove: + to_remove = [] + + for p in transaction.to_remove: + to_remove.append(UpgradeRequirement(pkg=p, required_size=0, extra_size=p.transaction_size)) + + to_remove.sort(key=attrgetter('pkg.name')) + to_remove = tuple(to_remove) + + to_upgrade = None + + if transaction.to_upgrade: + to_upgrade = [] + + for p in transaction.to_upgrade: + p_data = update_data.get(p.name) if update_data else None + + if p_data: + req_size = p_data.get('compressed size') + else: + req_size = None + + to_upgrade.append(UpgradeRequirement(pkg=p, required_size=req_size, extra_size=p.transaction_size)) + + to_upgrade.sort(key=attrgetter('pkg.name')) + to_upgrade = tuple(to_upgrade) + + return UpgradeRequirements(to_install=to_install, to_upgrade=to_upgrade, + to_remove=to_remove, cannot_upgrade=None) + else: + cannot_upgrade = [UpgradeRequirement(pkg=p, reason=self._i18n['error']) for p in pkgs] + cannot_upgrade.sort(key=attrgetter('pkg.name')) + return UpgradeRequirements(cannot_upgrade=cannot_upgrade, to_install=None, to_remove=None, + to_upgrade=[]) + + def get_managed_types(self) -> Set[Type[SoftwarePackage]]: + if self._types is None: + self._types = {DebianPackage} + + return self._types + + def get_info(self, pkg: DebianPackage) -> Optional[dict]: + info = {'00.name': pkg.name, '01.version': pkg.version, + "02.description": pkg.description} + + if pkg.installed and pkg.app: + info['03.exec'] = pkg.app.exe_path + + extra_info = self.aptitude.show((f'{pkg.name}={pkg.version}',), verbose=True) + + if extra_info: + extra_info = extra_info.get(pkg.name) + + if extra_info: + ignored_fields = {'package', 'version', 'description'} + + for field, val in extra_info.items(): + if field not in ignored_fields and not field.startswith('description'): + final_val = get_human_size_str(val) if field in self.aptitude.size_attrs else val + final_field = f'04.{field}' + + if final_field not in self._i18n: + self._i18n.default[final_field] = field + + info[final_field] = final_val # for sorting + + return info + + def get_history(self, pkg: SoftwarePackage) -> PackageHistory: + pass + + def install(self, pkg: SoftwarePackage, root_password: str, disk_loader: Optional[DiskCacheLoader], + watcher: ProcessWatcher) -> TransactionResult: + + watcher.change_substatus(self._i18n['debian.simulate_operation']) + transaction = self.aptitude.simulate_installation((pkg.name, )) + + if transaction is None or not transaction.to_install: + return TransactionResult.fail() + + if transaction.to_remove or (transaction.to_install and len(transaction.to_install) > 1): + watcher.change_substatus(self._i18n['debian.transaction.get_data']) + + deps = tuple(p for p in transaction.to_install or () if p.name != pkg.name) + removal = tuple(p for p in transaction.to_remove or ()) + all_pkgs = [*deps, *removal] + + pkgs_data = self.aptitude.show(pkgs=(f'{d.name}={d.version}' for d in all_pkgs), + attrs=self.install_show_attrs) + + if pkgs_data: + for p in all_pkgs: + fill_show_data(p, pkgs_data.get(p.name)) + + if not self.view.confirm_transaction(to_install=deps, removal=removal, watcher=watcher): + return TransactionResult.fail() + + watcher.change_substatus(self._i18n['debian.installing_pkgs']) + handler = ProcessHandler(watcher) + + targets = (p.name for p in transaction.to_install) + with self.output_handler.start(watcher=watcher, targets=targets, action=AptitudeAction.INSTALL) as handle: + installed, _ = handler.handle_simple(self.aptitude.install(packages=(pkg.name,), + root_password=root_password), + output_handler=handle) + + if installed: + self._refresh_apps_index(watcher) + + watcher.change_substatus(self._i18n['debian.install.validating']) + + currently_installed = set(self.aptitude.read_installed_names()) + + installed_instances = [] + if currently_installed: + for p in transaction.to_install: + instance = p if p != pkg else pkg + if instance.name in currently_installed: + instance.installed = True + instance.bind_app(self.apps_index.get(instance.name)) + installed_instances.append(instance) + + removed = None + + if transaction.to_remove: + removed = [p for p in transaction.to_remove if p.name not in currently_installed] + + not_removed = set(transaction.to_remove).difference(removed) + + if not_removed: + not_removed_str = ' '.join(p.name for p in not_removed) + self._log.warning(f"The following packages were not removed: {not_removed_str}") + + return TransactionResult(installed=installed_instances, removed=removed, + success=bool(installed_instances and pkg in installed_instances)) + else: + watcher.change_substatus('') + return TransactionResult.fail() + + def _refresh_apps_index(self, watcher: ProcessWatcher): + watcher.change_substatus(self._i18n['debian.app_index.checking']) + self._log.info("Reading the cached Debian applications") + indexed_apps = self.app_indexer.read_index() + self._log.info("Mapping the Debian applications") + current_apps = self.app_mapper.map_executable_applications() + + if current_apps != indexed_apps: + watcher.print(self._i18n['debian.app_index.updating'] + '...') + watcher.change_substatus(self._i18n['debian.app_index.updating']) + + try: + self.app_indexer.update_index(current_apps) + self._update_apps_index(current_apps) + + if indexed_apps is not None: + new_apps = current_apps.difference(indexed_apps) + + if new_apps: + self._log.info(f"Debian applications added to the index: " + f"{','.join((a.name for a in new_apps))}") + + except ApplicationIndexError: + pass + + def is_enabled(self) -> bool: + return self._enabled + + def set_enabled(self, enabled: bool): + self._enabled = enabled + + def can_work(self) -> Tuple[bool, Optional[str]]: + if not which('aptitude'): + return False, self._i18n['missing_dep'].format(dep=bold('aptitude')) + + return True, None + + def requires_root(self, action: SoftwareAction, pkg: Optional[SoftwarePackage]) -> bool: + if action == action.PREPARE: + deb_config = self.configman.get_config() + return SynchronizePackages.should_synchronize(deb_config, NullLoggerFactory.logger()) + + return action != SoftwareAction.SEARCH + + def prepare(self, task_manager: Optional[TaskManager], root_password: Optional[str], + internet_available: Optional[bool]): + + deb_config = self.configman.get_config() + + if SynchronizePackages.should_synchronize(deb_config, self._log): + sync_pkgs = SynchronizePackages(taskman=task_manager, i18n=self._i18n, logger=self._log, + root_password=root_password, aptitude=self.aptitude) + sync_pkgs.start() + + if DebianSuggestionsDownloader.should_download(deb_config, self._log, only_positive_exp=True): + self.suggestions_downloader.register_task(task_manager) + self.suggestions_downloader.start() + + self.index_apps(root_password=root_password, watcher=None, taskman=task_manager, + deb_config=deb_config, check_expiration=True) + + def list_updates(self, internet_available: bool) -> List[PackageUpdate]: + ignored_updates = set() + fill_ignored_updates = Thread(target=self._fill_ignored_updates, args=(ignored_updates,)) + fill_ignored_updates.start() + + updates = list() + for name, version in self.aptitude.read_updates(): + if fill_ignored_updates.is_alive(): + fill_ignored_updates.join() + + if name not in ignored_updates: + updates.append(PackageUpdate(pkg_id=name, name=name, version=version, pkg_type='debian')) + + return updates + + def list_warnings(self, internet_available: bool) -> Optional[List[str]]: + pass + + def _fill_installed_names(self, output: Set[str]): + output.update(self.aptitude.read_installed_names()) + + def _fill_suggestions(self, output: Dict[str, int]): + self.suggestions_downloader.register_task(None) + suggestions = self.suggestions_downloader.read(self.configman.read_config()) + + if suggestions: + output.update(suggestions) + + def list_suggestions(self, limit: int, filter_installed: bool) -> List[PackageSuggestion]: + name_priority = dict() + + fill_suggestions = Thread(target=self._fill_suggestions, args=(name_priority,)) + fill_suggestions.start() + + if filter_installed: + installed = set() + fill_installed = Thread(target=self._fill_installed_names, args=(installed, )) + fill_installed.start() + else: + installed, fill_installed = None, None + + fill_suggestions.join() + + if fill_installed: + fill_installed.join() + + if not name_priority: + self._log.info("No Debian package suggestions found") + return [] + + self._log.info(f"Found {len(name_priority)} Debian package suggestions") + + to_load = tuple(name_priority.keys()) if not installed else {*name_priority.keys()}.difference(installed) + + if not to_load: + return [] + + suggestions = [] + + for pkg in self.aptitude.search_by_name(to_load): + prio = name_priority.get(pkg.name) + + if prio: + suggestions.append(PackageSuggestion(package=pkg, priority=prio)) + + return suggestions + + def is_default_enabled(self) -> bool: + return True + + def launch(self, pkg: SoftwarePackage): + if isinstance(pkg, DebianPackage): + final_cmd = pkg.app.exe_path.replace('%U', '') + Popen(final_cmd, shell=True) + + def get_settings(self, screen_width: int, screen_height: int) -> Optional[ViewComponent]: + deb_config = self.configman.get_config() + + comps_width = int(screen_width * 0.105) + + sources_app = deb_config.get('pkg_sources.app') + + if isinstance(sources_app, str) and sources_app not in self.known_sources_apps: + self._log.warning(f"'pkg_sources.app' ({sources_app}) is not supported. A 'None' value will be considered") + sources_app = None + + lb_source_auto = self._i18n['debian.config.pkg_sources.app.auto'] + source_opts = [InputOption(id_='auto', value=None, label=lb_source_auto)] + + source_opts.extend((InputOption(id_=a, value=a, label=a) for a in self.known_sources_apps if which(a))) + + source_auto_tip = self._i18n['debian.config.pkg_sources.app.tip'].format(auto=f'"{lb_source_auto}"') + input_sources = SingleSelectComponent(id_='pkg_sources.app', + label=self._i18n['debian.config.pkg_sources.app'], + tooltip=source_auto_tip, + options=source_opts, + default_option=next(o for o in source_opts if o.value == sources_app), + type_=SelectViewType.COMBO, + max_width=comps_width) + + try: + app_cache_exp = int(deb_config.get('index_apps.exp', 0)) + except ValueError: + self._log.error(f"Unexpected value form Debian configuration property 'index_apps.exp': " + f"{deb_config['index_apps.exp']}. Zero (0) will be considered instead.") + app_cache_exp = 0 + + ti_index_apps_exp = TextInputComponent(id_='index_apps.exp', + label=self._i18n['debian.config.index_apps.exp'], + tooltip=self._i18n['debian.config.index_apps.exp.tip'], + value=str(app_cache_exp), only_int=True, + max_width=comps_width) + + try: + sync_pkgs_time = int(deb_config.get('sync_pkgs.time', 0)) + except ValueError: + self._log.error(f"Unexpected value form Debian configuration property 'sync_pkgs.time': {deb_config['sync_pkgs.time']}. " + f"Zero (0) will be considered instead.") + sync_pkgs_time = 0 + + ti_sync_pkgs = TextInputComponent(id_='sync_pkgs.time', + label=self._i18n['debian.config.sync_pkgs.time'], + tooltip=self._i18n['debian.config.sync_pkgs.time.tip'], + value=str(sync_pkgs_time), only_int=True, + max_width=comps_width) + + try: + suggestions_exp = int(deb_config.get('suggestions.exp', 0)) + except ValueError: + self._log.error(f"Unexpected value form Debian configuration property 'suggestions.exp': {deb_config['suggestions.exp']}. " + f"Zero (0) will be considered instead.") + suggestions_exp = 0 + + ti_suggestions_exp = TextInputComponent(id_='suggestions.exp', + label=self._i18n['debian.config.suggestions.exp'], + tooltip=self._i18n['debian.config.suggestions.exp.tip'], + value=str(suggestions_exp), only_int=True, + max_width=comps_width) + + return PanelComponent([FormComponent([input_sources, ti_sync_pkgs, ti_index_apps_exp, ti_suggestions_exp])]) + + def save_settings(self, component: PanelComponent) -> Tuple[bool, Optional[List[str]]]: + deb_config = self.configman.get_config() + + container = component.components[0] + + if isinstance(container, FormComponent): + deb_config['pkg_sources.app'] = container.get_single_select_component('pkg_sources.app').get_selected() + deb_config['index_apps.exp'] = container.get_text_input('index_apps.exp').get_int_value() + deb_config['sync_pkgs.time'] = container.get_text_input('sync_pkgs.time').get_int_value() + deb_config['suggestions.exp'] = container.get_text_input('suggestions.exp').get_int_value() + + try: + self.configman.save_config(deb_config) + return True, None + except: + return False, [traceback.format_exc()] + + def gen_custom_actions(self) -> Generator[CustomSoftwareAction, None, None]: + if self._default_actions is None: + self._default_actions = (CustomSoftwareAction(i18n_label_key='debian.action.sync_pkgs', + i18n_status_key='debian.task.sync_pkgs.status', + i18n_description_key='debian.action.sync_pkgs.desc', + icon_path=DEBIAN_ICON_PATH, + manager_method='synchronize_packages', + requires_root=True), + CustomSoftwareAction(i18n_label_key='debian.action.index_apps', + i18n_status_key='debian.task.app_index.status', + i18n_description_key='debian.action.index_apps.desc', + icon_path=DEBIAN_ICON_PATH, + manager_method='index_apps', + requires_root=False) + ) + + yield from self._default_actions + + for _ in self.get_installed_source_apps(): + yield self.action_launch_sources + break + + def _write_ignored_updates(self, packages: Iterable[str]): + Path(os.path.dirname(self.file_ignored_updates)).mkdir(parents=True, exist_ok=True) + + with open(self.file_ignored_updates, 'w+') as f: + f.write('\n'.join(n for n in sorted(packages))) + + def ignore_update(self, pkg: DebianPackage): + ignored_packages = set() + self._fill_ignored_updates(ignored_packages) + + if pkg.name not in ignored_packages: + pkg.updates_ignored = True + ignored_packages.add(pkg.name) + self._write_ignored_updates(ignored_packages) + + def revert_ignored_update(self, pkg: DebianPackage): + ignored_packages = set() + self._fill_ignored_updates(ignored_packages) + + if pkg.name in ignored_packages: + pkg.updates_ignored = False + ignored_packages.remove(pkg.name) + self._write_ignored_updates(ignored_packages) + + def synchronize_packages(self, root_password: Optional[str], watcher: ProcessWatcher) -> bool: + return SynchronizePackages(i18n=self._i18n, logger=self._log, root_password=root_password, watcher=watcher, + aptitude=self.aptitude, taskman=TaskManager()).run() + + def index_apps(self, root_password: Optional[str], watcher: Optional[ProcessWatcher], + taskman: Optional[TaskManager] = None, deb_config: Optional[dict] = None, + check_expiration: bool = False) -> bool: + _config = deb_config if deb_config else self.configman.get_config() + _taskman = taskman if taskman else TaskManager() + + map_apps = MapApplications(taskman=_taskman, app_indexer=self.app_indexer, + i18n=self._i18n, logger=self._log, deb_config=deb_config, + app_mapper=self.app_mapper, check_expiration=check_expiration, + watcher=watcher) + map_apps.start() + + gen_app_index = UpdateApplicationIndex(taskman=_taskman, app_indexer=self.app_indexer, + i18n=self._i18n, logger=self._log, + mapping_apps=map_apps) + gen_app_index.start() + + map_apps.join() + self._update_apps_index(map_apps.apps) + gen_app_index.join() + return True + + def purge(self, pkg: DebianPackage, root_password: str, watcher: ProcessWatcher) -> bool: + if not self.view.confirm_purge(pkg.name, watcher): + return False + + res = self.uninstall(pkg=pkg, root_password=root_password, watcher=watcher, disk_loader=None, purge=True) + return res.success + + def launch_sources_app(self, root_password: str, watcher: ProcessWatcher) -> bool: + deb_config = self.configman.get_config() + + sources_app = deb_config.get('pkg_sources.app') + + if isinstance(sources_app, str) and sources_app not in self.known_sources_apps: + watcher.show_message(title=self._i18n['popup.title.error'], + body=self._i18n['debian.action.sources.unsupported'].format(app=bold(sources_app))) + return False + + if sources_app: + if not which(sources_app): + watcher.show_message(title=self._i18n['popup.title.error'], + body=self._i18n['debian.action.sources.not_installed'], + type_=MessageType.ERROR) + return False + + Popen(sources_app, shell=True) + return True + + for app in self.get_installed_source_apps(): + Popen(app, shell=True) + return True + + watcher.show_message(title=self._i18n['popup.title.error'], + body=self._i18n['debian.action.sources.not_installed'], + type_=MessageType.ERROR) + return False + + @property + def app_indexer(self) -> ApplicationIndexer: + if self._app_indexer is None: + self._app_indexer = ApplicationIndexer(self._log) + + return self._app_indexer + + @property + def apps_index(self) -> Dict[str, DebianApplication]: + if self._apps_index is None: + self._update_apps_index(self.app_indexer.read_index()) + + return self._apps_index + + @property + def install_show_attrs(self) -> Set[str]: + if self._install_show_attrs is None: + self._install_show_attrs = {'description', 'maintainer', 'section', 'compressed size'} + + return self._install_show_attrs + + @property + def configman(self) -> DebianConfigManager: + if self._configman is None: + self._configman = DebianConfigManager() + + return self._configman + + @property + def view(self) -> DebianViewBridge: + if self._view is None: + self._view = DebianViewBridge(screen_width=self.context.screen_width, + screen_heigth=self.context.screen_height, + i18n=self._i18n) + + return self._view + + @property + def app_mapper(self) -> ApplicationsMapper: + if self._app_mapper is None: + self._app_mapper = ApplicationsMapper(logger=self._log) + + return self._app_mapper + + @property + def aptitude(self) -> Aptitude: + if self._aptitude is None: + self._aptitude = Aptitude(self._log) + + return self._aptitude + + @property + def output_handler(self) -> AptitudeOutputHandlerFactory: + if self._output_handler is None: + self._output_handler = AptitudeOutputHandlerFactory(i18n=self._i18n) + + return self._output_handler + + @property + def action_launch_sources(self) -> CustomSoftwareAction: + if self._action_launch_sources is None: + self._action_launch_sources = CustomSoftwareAction(i18n_label_key='debian.action.sources', + i18n_status_key='debian.task.sources.status', + i18n_description_key='debian.action.sources.desc', + icon_path=DEBIAN_ICON_PATH, + manager_method='launch_sources_app', + requires_confirmation=False, + requires_root=False) + + return self._action_launch_sources + + @property + def known_sources_apps(self) -> Tuple[str, ...]: + if self._known_sources_apps is None: + self._known_sources_apps = ('software-properties-gtk',) + + return self._known_sources_apps + + @property + def file_ignored_updates(self) -> str: + if self._file_ignored_updates is None: + self._file_ignored_updates = f'{CONFIG_DIR}/debian/updates_ignored.txt' + + return self._file_ignored_updates + + @property + def suggestions_downloader(self) -> DebianSuggestionsDownloader: + if not self._suggestions_downloader: + self._suggestions_downloader = DebianSuggestionsDownloader(i18n=self._i18n, logger=self._log, + http_client=self.context.http_client) + + return self._suggestions_downloader + + def get_installed_source_apps(self) -> Generator[str, None, None]: + for app in self.known_sources_apps: + if shutil.which(app): + yield app diff --git a/bauh/gems/debian/gui.py b/bauh/gems/debian/gui.py new file mode 100644 index 00000000..aa279301 --- /dev/null +++ b/bauh/gems/debian/gui.py @@ -0,0 +1,140 @@ +from operator import attrgetter +from typing import Collection, Optional, Tuple, List + +from bauh.api.abstract.handler import ProcessWatcher +from bauh.api.abstract.view import InputOption, MultipleSelectComponent, TextComponent +from bauh.commons.html import bold +from bauh.commons.system import get_human_size_str +from bauh.gems.debian import DEBIAN_ICON_PATH +from bauh.gems.debian.model import DebianPackage +from bauh.view.util.translation import I18n + + +class DebianViewBridge: + + def __init__(self, screen_width: int, screen_heigth: int, i18n: I18n): + self._i18n = i18n + self._width = screen_width + self._height = screen_heigth + + def _map_to_install(self, pkgs: Optional[Collection[DebianPackage]]) -> Optional[Tuple[List[InputOption], str, str]]: + if pkgs: + download_size, install_size = 0, 0 + + views = [] + for p in pkgs: + if p.compressed_size is not None and p.compressed_size >= 0: + compressed = get_human_size_str(p.compressed_size) + download_size += p.compressed_size + + else: + compressed = '?' + + if p.transaction_size is not None: + install_size += p.transaction_size + uncompressed = get_human_size_str(p.transaction_size) + else: + uncompressed = '?' + + views.append(InputOption(label=f"{p.name} ({compressed} | {uncompressed})", + value=p.name, read_only=True, icon_path=DEBIAN_ICON_PATH, + tooltip=p.description if p.description else '?')) + + dsize = get_human_size_str(download_size) if download_size > 0 else '?' + isize = get_human_size_str(install_size) if install_size > 0 else '?' + return views, dsize, isize + + def _map_to_remove(self, pkgs: Optional[Collection[DebianPackage]]) -> Optional[Tuple[List[InputOption], str]]: + if pkgs: + freed_space = 0 + + views = [] + for p in pkgs: + if p.transaction_size is not None: + size = p.transaction_size * (-1 if p.transaction_size < 0 else 1) + freed_space += size + uncompressed = get_human_size_str(size) + else: + uncompressed = '?' + + views.append(InputOption(label=f"{p.name} (-{uncompressed})", + value=p.name, read_only=True, icon_path=DEBIAN_ICON_PATH, + tooltip=p.description if p.description else '?')) + + return views, f'-{get_human_size_str(freed_space)}' if freed_space > 0 else '?' + + def confirm_transaction(self, to_install: Optional[Collection[DebianPackage]], + removal: Optional[Collection[DebianPackage]], + watcher: ProcessWatcher) -> bool: + + components = [] + + to_remove_data = self._map_to_remove(removal) + + text_width, select_width = 672, 595 + + if to_remove_data: + to_remove_data[0].sort(key=attrgetter('label')) + lb_rem = self._i18n['debian.transaction.to_remove'].format(no=bold(str(len(to_remove_data[0]))), + fspace=bold(to_remove_data[1])) + + components.append(TextComponent(html=lb_rem, min_width=text_width)) + components.append(MultipleSelectComponent(id_='rem', options=to_remove_data[0], label=None, + default_options={*to_remove_data[0]}, + max_width=select_width)) + + to_install_data = self._map_to_install(to_install) + + if to_install_data: + to_install_data[0].sort(key=attrgetter('label')) + lb_deps = self._i18n['debian.transaction.to_install'].format(no=bold(str(len(to_install_data[0]))), + dsize=bold(to_install_data[1]), + isize=bold(to_install_data[2])) + + components.append(TextComponent(html=f'
{lb_deps}', min_width=text_width)) + components.append(MultipleSelectComponent(id_='inst', label='', options=to_install_data[0], + default_options={*to_install_data[0]}, + max_width=select_width)) + + return watcher.request_confirmation(title=self._i18n['debian.transaction.title'], + components=components, + confirmation_label=self._i18n['popup.button.continue'], + deny_label=self._i18n['popup.button.cancel'], + body=None, + min_width=text_width, + min_height=54) + + def confirm_removal(self, source_pkg: str, dependencies: Collection[DebianPackage], watcher: ProcessWatcher) -> bool: + dep_views = [] + freed_space = 0 + for p in sorted(dependencies, key=attrgetter('name')): + if p.transaction_size is not None: + size = p.transaction_size * (-1 if p.transaction_size < 0 else 1) + freed_space += size + size_str = get_human_size_str(size) + else: + size_str = '?' + + dep_views.append(InputOption(label=f"{p.name}: -{size_str}", value=p.name, read_only=True, + icon_path=DEBIAN_ICON_PATH, tooltip=p.description)) + + deps_container = MultipleSelectComponent(id_='deps', label='', options=dep_views, default_options={*dep_views}, + max_width=537) + + freed_space_str = bold('-' + get_human_size_str(freed_space)) + body_text = TextComponent(html=self._i18n['debian.remove_deps'].format(no=bold(str(len(dependencies))), + pkg=bold(source_pkg), + fspace=freed_space_str), + min_width=653) + + return watcher.request_confirmation(title=self._i18n['debian.transaction.title'], + components=[body_text, deps_container], + confirmation_label=self._i18n['popup.button.continue'], + deny_label=self._i18n['popup.button.cancel'], + body=None) + + def confirm_purge(self, pkg_name: str, watcher: ProcessWatcher) -> bool: + msg = self._i18n['debian.action.purge.confirmation'].format(pkg=bold(pkg_name)) + return watcher.request_confirmation(title=self._i18n['debian.action.purge'], + body=msg, + confirmation_label=self._i18n['popup.button.continue']) diff --git a/bauh/gems/debian/index.py b/bauh/gems/debian/index.py new file mode 100644 index 00000000..f2f8418d --- /dev/null +++ b/bauh/gems/debian/index.py @@ -0,0 +1,223 @@ +import json +import os +import re +import traceback +from concurrent.futures import ThreadPoolExecutor +from datetime import datetime, timedelta +from json import JSONDecodeError +from logging import Logger +from pathlib import Path +from typing import Optional, Set, Generator, Iterable + +from bauh.commons import system +from bauh.gems.debian import APP_INDEX_FILE +from bauh.gems.debian.model import DebianApplication + + +class ApplicationIndexError(Exception): + + def __init__(self, cause: Optional[str] = None): + self.cause = cause + + +class ApplicationIndexer: + + def __init__(self, logger: Logger, index_file_path: str = APP_INDEX_FILE): + self._log = logger + self._file_path = index_file_path + self._file_timestamp_path = f'{self._file_path}.ts' + + def is_expired(self, deb_config: dict) -> bool: + + try: + exp_minutes = int(deb_config.get('index_apps.exp', 0)) + except ValueError: + self._log.error(f"Invalid value for Debian configuration property 'index_apps.exp': " + f"{deb_config['index_apps.exp']}") + return True + + if exp_minutes <= 0: + self._log.warning("Debian applications index will always be updated ('index_apps.exp' <= 0 )'") + return True + + if not os.path.exists(self._file_path): + self._log.info(f"Debian applications index not found. A new one must be generated ({self._file_path})") + return True + + try: + with open(self._file_timestamp_path) as f: + timestamp_str = f.read().strip() + except FileNotFoundError: + self._log.info(f"Debian applications index timestamp not found ({self._file_timestamp_path})") + return True + + try: + timestamp = datetime.fromtimestamp(float(timestamp_str)) + except: + self._log.error(f'Could not parse the Debian applications index timestamp: {timestamp_str} ' + f'({self._file_timestamp_path})') + traceback.print_exc() + return True + + expired = timestamp + timedelta(minutes=exp_minutes) <= datetime.utcnow() + + if expired: + self._log.info("Debian applications index has expired. A new one must be generated.") + else: + self._log.info("Debian applications index is up-to-date") + + return expired + + def read_index(self) -> Generator[DebianApplication, None, None]: + try: + with open(self._file_path) as f: + idx_str = f.read().strip() + + if not idx_str: + self._log.warning(f"Debian applications index is empty ({self._file_path})") + else: + try: + for name, data in json.loads(idx_str).items(): + exe_path, icon_path = data.get('exe_path'), data.get('icon_path') + + if not all((name, exe_path, icon_path)): + self._log.warning(f"Invalid entry in the Debian applications index ({self._file_path}): name={name}, exe_path={exe_path}, icon_path={icon_path}") + else: + categories = data.get('categories') + yield DebianApplication(name=name, exe_path=exe_path, icon_path=icon_path, + categories=tuple(categories) if categories else None) + + except JSONDecodeError: + self._log.error(f"The Debian applications index is corrupted ({self._file_path}). " + f"Could not decode the JSON.") + + except FileNotFoundError: + self._log.warning(f"Debian applications index not found ({self._file_path})") + except OSError as e: + self._log.error(f"Debian applications index could not be read ({self._file_path}). OSError: {e.errno}") + + def update_index(self, apps: Set[DebianApplication], update_timestamp: bool = True): + idx_dir = os.path.dirname(self._file_path) + + try: + Path(idx_dir).mkdir(exist_ok=True, parents=True) + except OSError: + self._log.error(f"Could not create directory '{idx_dir}'") + raise ApplicationIndexError() + + idx = {} + if apps: + for app in apps: + idx.update(app.to_index()) + + try: + with open(self._file_path, 'w+') as f: + if idx: + f.write(json.dumps(idx, sort_keys=True, indent=4)) + else: + f.write('') + + except OSError: + self._log.error(f"Could not write to the Debian applications index file: {self._file_path}") + raise ApplicationIndexError() + + if update_timestamp: + index_timestamp = datetime.utcnow().timestamp() + try: + with open(self._file_timestamp_path, 'w+') as f: + f.write(str(index_timestamp)) + + self._log.info("Debian applications index timestamp updated") + + except OSError: + self._log.error(f"Could not write to the Debian applications index timestamp file: " + f"{self._file_timestamp_path}") + raise ApplicationIndexError() + + +class ApplicationsMapper: + + def __init__(self, logger: Logger, workers: int = 10): + self._log = logger + self._re_desktop_file = re.compile(r'(.+):\s+(/usr/share/applications/.+\.desktop)') + self._re_desktop_file_fields = re.compile('(Exec|TryExec|Icon|Categories|NoDisplay|Terminal)\s*=\s*(.+)') + self._workers = workers + + def _read_file(self, file_path: str) -> Optional[str]: + try: + with open(file_path) as file: + return file.read() + except (FileNotFoundError, OSError) as e: + self._log.error(f"Error when checking desktop file '{file_path}' ({file_path}):" + f" {e.__class__.__name__}") + + def _add_if_application_desktop_file(self, pkg_name: str, desktop_files: Iterable[str], output: Set[DebianApplication]): + for file_path in sorted(desktop_files): + content = self._read_file(file_path) + + if content: + data = {} + + gui_app = True + + for f, v in self._re_desktop_file_fields.findall(content): + if f in ('NoDisplay', 'Terminal') and v.strip().lower() == 'true': + gui_app = False + break + + if f not in data: + data[f.strip()] = v.strip() + + if not gui_app: + continue + + exe = data.get('Exec') + + if not exe: + exe = data.get('TryExec') + + if not exe: + continue + + icon = data.get('Icon') + + if not icon: + continue + + categories = data.get('Categories') + + if categories: + categories = tuple(sorted({c.strip() for c in categories.split(';') if c})) + + output.add(DebianApplication(name=pkg_name, exe_path=exe, icon_path=icon, categories=categories)) + break + + def map_executable_applications(self) -> Optional[Set[DebianApplication]]: + exitcode, output = system.execute('dpkg-query -S .desktop', shell=True) + + if exitcode == 0 and output: + + pkg_files = dict() + + for found in self._re_desktop_file.findall(output): + pkg_name = found[0].strip() + files = pkg_files.get(pkg_name) + + if files is None: + files = set() + pkg_files[pkg_name] = files + + files.add(found[1].strip()) + + if pkg_files: + apps_found, check_jobs = set(), [] + + with ThreadPoolExecutor(self._workers) as pool: + for pkg_name, files in pkg_files.items(): + check_jobs.append(pool.submit(self._add_if_application_desktop_file, + pkg_name, files, apps_found)) + + for job in check_jobs: + job.done() + + return apps_found diff --git a/bauh/gems/debian/model.py b/bauh/gems/debian/model.py new file mode 100644 index 00000000..6fcac582 --- /dev/null +++ b/bauh/gems/debian/model.py @@ -0,0 +1,167 @@ +from typing import Optional, Tuple, Collection, Iterable, Generator + +from bauh.api.abstract.model import SoftwarePackage, CustomSoftwareAction +from bauh.commons import resource +from bauh.gems.debian import DEBIAN_ICON_PATH, ROOT_DIR + + +class DebianApplication: + """ + For packages that represent applications + """ + + def __init__(self, name: str, exe_path: str, icon_path: str, categories: Optional[Tuple] = None): + self.name = name + self.exe_path = exe_path + self.icon_path = icon_path + self.categories = categories + + def __eq__(self, other): + if isinstance(other, DebianApplication): + return self.__dict__ == other.__dict__ + + return False + + def __hash__(self) -> int: + hash_sum = 0 + + for k, v in self.__dict__.items(): + hash_sum += hash(v) + + return hash_sum + + def __repr__(self): + return f"{self.__class__.__name__} ({', '.join((f'{k}={v}' for k, v in self.__dict__.items()))})" + + def to_index(self) -> dict: + return {self.name: {f: v for f, v in self.__dict__.items() if f != 'name'}} + + +class DebianPackage(SoftwarePackage): + + __actions: Optional[Tuple[CustomSoftwareAction]] = None + + @classmethod + def custom_actions(cls) -> Tuple[CustomSoftwareAction]: + if cls.__actions is None: + cls.__actions = ((CustomSoftwareAction(i18n_label_key='debian.action.purge', + i18n_status_key='debian.action.purge.status', + i18n_description_key='debian.action.purge.desc', + icon_path=resource.get_path('img/clean.svg', ROOT_DIR), + manager_method='purge', + requires_root=True, + requires_confirmation=False),)) + + return cls.__actions + + def __init__(self, name: str = None, version: Optional[str] = None, latest_version: Optional[str] = None, + description: Optional[str] = None, maintainer: Optional[str] = None, installed: bool = False, + update: bool = False, app: Optional[DebianApplication] = None, compressed_size: Optional[int] = None, + uncompressed_size: Optional[int] = None, categories: Tuple[str] = None, + updates_ignored: Optional[bool] = None, transaction_size: Optional[int] = None): + super(DebianPackage, self).__init__(id=name, name=name, version=version, installed=installed, + description=description, update=update, + latest_version=latest_version if latest_version is not None else version) + self.maintainer = maintainer + self.compressed_size = compressed_size + self.uncompressed_size = uncompressed_size + self.categories = categories + self.app = app + self.bind_app(app) + self.updates_ignored = updates_ignored + self.transaction_size = transaction_size # size in bytes related to a transaction (install, upgrade, remove) + + def bind_app(self, app: Optional[DebianApplication]): + self.app = app + + if app and app.categories: + self.categories = app.categories + + def has_history(self) -> bool: + return False + + def has_screenshots(self) -> bool: + return False + + def has_info(self) -> bool: + return True + + def can_be_downgraded(self) -> bool: + return False + + def get_type(self): + return 'debian' + + def get_default_icon_path(self) -> str: + return self.get_type_icon_path() + + def get_type_icon_path(self) -> str: + return DEBIAN_ICON_PATH + + def is_application(self): + return bool(self.app) + + def get_data_to_cache(self) -> dict: + pass + + def fill_cached_data(self, data: dict): + pass + + def can_be_run(self) -> bool: + return bool(self.app) + + def get_publisher(self) -> str: + return self.maintainer + + def supports_backup(self) -> bool: + return True + + def get_disk_icon_path(self) -> Optional[str]: + if self.app: + return self.app.icon_path + + def get_custom_actions(self) -> Optional[Iterable[CustomSoftwareAction]]: + if self.installed: + return self.custom_actions() + + def is_update_ignored(self) -> bool: + return bool(self.updates_ignored) + + def supports_ignored_updates(self) -> bool: + return True + + def __eq__(self, other): + if isinstance(other, DebianPackage): + return self.name == other.name + + return False + + def __hash__(self): + return hash(self.name) + + def __repr__(self) -> str: + attrs = ', '.join((f'{p}={v}' for p, v in sorted(self.__dict__.items()))) + return f"{self.__class__.__name__} ({attrs})" + + +class DebianTransaction: + + def __init__(self, to_install: Optional[Collection[DebianPackage]], + to_remove: Optional[Collection[DebianPackage]], + to_upgrade: Optional[Collection[DebianPackage]]): + + self.to_install = to_install + self.to_remove = to_remove + self.to_upgrade = to_upgrade + + @property + def all_packages(self) -> Generator[DebianPackage, None, None]: + for pkgs in (self.to_install, self.to_remove, self.to_upgrade): + if pkgs: + yield from pkgs + + def __eq__(self, other) -> bool: + return self.__dict__ == other.__dict__ if isinstance(other, DebianTransaction) else False + + def __hash__(self) -> int: + return sum((hash(v)for v in self.__dict__.values())) diff --git a/bauh/gems/debian/resources/img/clean.svg b/bauh/gems/debian/resources/img/clean.svg new file mode 100644 index 00000000..fba112a9 --- /dev/null +++ b/bauh/gems/debian/resources/img/clean.svg @@ -0,0 +1,158 @@ + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bauh/gems/debian/resources/img/debian.svg b/bauh/gems/debian/resources/img/debian.svg new file mode 100644 index 00000000..c112ee02 --- /dev/null +++ b/bauh/gems/debian/resources/img/debian.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bauh/gems/debian/resources/locale/ca b/bauh/gems/debian/resources/locale/ca new file mode 100644 index 00000000..79fb13f6 --- /dev/null +++ b/bauh/gems/debian/resources/locale/ca @@ -0,0 +1,84 @@ +debian.action.index_apps=Index applications +debian.action.index_apps.desc=Identifies all installed packages associated with .desktop files and maps them as applications +debian.action.purge=Remove completely +debian.action.purge.confirmation=The complete removal of {pkg} will erase the software files and also the configuration ones +debian.action.purge.desc=Removes all software and configuration files associated with the package +debian.action.purge.status=Removing {} completely +debian.action.sync_pkgs=Synchronize packages +debian.action.sync_pkgs.desc=Synchronizes the available software packages on the repositories +debian.action.sync_pkgs.status=Synchronizing packages +debian.action.sources=Software sources +debian.action.sources.desc=Starts the application for managing software sources +debian.action.sources.not_installed=The application for managing software sources is not installed +debian.action.sources.status=Launching application +debian.action.sources.unsupported=The application {app} is not supported and will not be started +debian.app_index.checking=Checking the apps index +debian.app_index.updating=Updating the apps index +debian.config.index_apps.exp=Apps cache expiration +debian.config.index_apps.exp.tip=Time period in MINUTES in which the installed applications cache is considered up-to-date during startup +debian.config.pkg_sources.app=Software sources +debian.config.pkg_sources.app.tip=Application for managing the software sources. {auto} detects one of the supported applications automatically. +debian.config.pkg_sources.app.auto=Auto +debian.config.suggestions.exp=Suggestions expiration +debian.config.suggestions.exp.tip=It defines the period (in hours) in which the suggestions stored in disc will be considered up to date. Use 0 if you always want to update them. +debian.config.sync_pkgs.time=Packages synchronization period +debian.config.sync_pkgs.time.tip=Time period in MINUTES in which the packages synchronization must be done on startup +debian.info.00.name=name +debian.info.01.version=version +debian.info.02.description=description +debian.info.03.exec=executable +debian.info.04.architecture=architecture +debian.info.04.architecture.all=all +debian.info.04.archive=archive +debian.info.04.automatically installed=automatically installed +debian.info.04.automatically installed.no=no +debian.info.04.automatically installed.yes=yes +debian.info.04.breaks=breaks +debian.info.04.compressed size=Download size +debian.info.04.conflicts=conflicts +debian.info.04.depends=dependencies +debian.info.04.filename=file +debian.info.04.homepage=web page +debian.info.04.maintainer=maintainer +debian.info.04.md5sum=MD5 +debian.info.04.multi-arch=multi-architecture +debian.info.04.multi-arch.foreign=foreign +debian.info.04.multi-arch.same=same +debian.info.04.replaces=replaces +debian.info.04.predepends=pre dependencies +debian.info.04.priority=priority +debian.info.04.priority.important=important +debian.info.04.priority.optional=optional +debian.info.04.provided by=provided by +debian.info.04.provides=provides +debian.info.04.recommends=recommends +debian.info.04.section=section +debian.info.04.state=state +debian.info.04.state.installed=installed +debian.info.04.state.not installed=not installed +debian.info.04.state.not installed (configuration files remain)=not installed (configuration files installed) +debian.info.04.suggests=suggests +debian.info.04.uncompressed size=installation size +debian.install.validating=Validating installed packages +debian.installing_pkgs=Installing packages +debian.output.downloading=Downloading {pkg} +debian.output.removing=Removing {pkg} +debian.output.finishing=Finishing +debian.output.unpacking=Unpacking {pkg} +debian.remove.impossible=It is not possible to remove the package {pkg} +debian.remove_deps=The following {no} dependent packages of {pkg} will be removed as well ({fspace}) +debian.simulate_operation=Simulating the operation +debian.task.app_index.status=Indexing installed applications +debian.task.map_apps.check_files=Checking {type} files +debian.task.map_apps.read_cache=Reading apps from the index (cache) +debian.task.map_apps.status=Mapping installed applications +debian.task.update_apps_idx.status=Updating index +debian.task.sync_pkgs.status=Synchronizing packages +debian.transaction.dependency_of=Dependency of {pkgs} +debian.transaction.get_data=Retrieving the operation's data +debian.transaction.title=Operation details +debian.transaction.to_install=The following additional packages ({no}) will be installed (Download: {dsize} | Installation: {isize}) +debian.transaction.to_remove=The following packages ({no}) will be removed (Freed space: {fspace}) +debian.uninstall.failed_to_remove=It was not possible to remove the following {no} packages: {pkgs} +debian.uninstall.removing=Removing packages +debian.uninstall.validating=Validating removed packages diff --git a/bauh/gems/debian/resources/locale/de b/bauh/gems/debian/resources/locale/de new file mode 100644 index 00000000..34c27357 --- /dev/null +++ b/bauh/gems/debian/resources/locale/de @@ -0,0 +1,84 @@ +debian.action.index_apps=Index applications +debian.action.index_apps.desc=Identifies all installed packages associated with .desktop files and maps them as applications +debian.action.purge=Remove completely +debian.action.purge.confirmation=The complete removal of {pkg} will erase the software files and also the configuration ones +debian.action.purge.desc=Removes all software and configuration files associated with the package +debian.action.purge.status=Removing {} completely +debian.action.sync_pkgs=Synchronize packages +debian.action.sync_pkgs.desc=Synchronizes the available software packages on the repositories +debian.action.sync_pkgs.status=Synchronizing packages +debian.action.sources=Software sources +debian.action.sources.desc=Starts the application for managing software sources +debian.action.sources.not_installed=The application for managing software sources is not installed +debian.action.sources.status=Launching application +debian.action.sources.unsupported=The application {app} is not supported and will not be started +debian.app_index.checking=Checking the apps index +debian.app_index.updating=Updating the apps index +debian.config.index_apps.exp=Apps cache expiration +debian.config.index_apps.exp.tip=Time period in MINUTES in which the installed applications cache is considered up-to-date during startup +debian.config.pkg_sources.app=Software sources +debian.config.pkg_sources.app.tip=Application for managing the software sources. {auto} detects one of the supported applications automatically. +debian.config.pkg_sources.app.auto=Auto +debian.config.suggestions.exp=Suggestions expiration +debian.config.suggestions.exp.tip=It defines the period (in hours) in which the suggestions stored in disc will be considered up to date. Use 0 if you always want to update them. +debian.config.sync_pkgs.time=Packages synchronization period +debian.config.sync_pkgs.time.tip=Time period in MINUTES in which the packages synchronization must be done on startup +debian.info.00.name=name +debian.info.01.version=version +debian.info.02.description=description +debian.info.03.exec=executable +debian.info.04.architecture=architecture +debian.info.04.architecture.all=all +debian.info.04.archive=archive +debian.info.04.automatically installed=automatically installed +debian.info.04.automatically installed.no=no +debian.info.04.automatically installed.yes=yes +debian.info.04.breaks=breaks +debian.info.04.compressed size=Download size +debian.info.04.conflicts=conflicts +debian.info.04.depends=dependencies +debian.info.04.filename=file +debian.info.04.homepage=web page +debian.info.04.maintainer=maintainer +debian.info.04.md5sum=MD5 +debian.info.04.multi-arch=multi-architecture +debian.info.04.multi-arch.foreign=foreign +debian.info.04.multi-arch.same=same +debian.info.04.replaces=replaces +debian.info.04.predepends=pre dependencies +debian.info.04.priority=priority +debian.info.04.priority.important=important +debian.info.04.priority.optional=optional +debian.info.04.provided by=provided by +debian.info.04.provides=provides +debian.info.04.recommends=recommends +debian.info.04.section=section +debian.info.04.state=state +debian.info.04.state.installed=installed +debian.info.04.state.not installed=not installed +debian.info.04.state.not installed (configuration files remain)=not installed (configuration files installed) +debian.info.04.suggests=suggests +debian.info.04.uncompressed size=installation size +debian.install.validating=Validating installed packages +debian.installing_pkgs=Installing packages +debian.output.downloading=Downloading {pkg} +debian.output.finishing=Finishing +debian.output.removing=Removing {pkg} +debian.output.unpacking=Unpacking {pkg} +debian.remove.impossible=It is not possible to remove the package {pkg} +debian.remove_deps=The following {no} dependent packages of {pkg} will be removed as well ({fspace}) +debian.simulate_operation=Simulating the operation +debian.task.app_index.status=Indexing installed applications +debian.task.map_apps.check_files=Checking {type} files +debian.task.map_apps.read_cache=Reading apps from the index (cache) +debian.task.map_apps.status=Mapping installed applications +debian.task.update_apps_idx.status=Updating index +debian.task.sync_pkgs.status=Synchronizing packages +debian.transaction.dependency_of=Dependency of {pkgs} +debian.transaction.get_data=Retrieving the operation's data +debian.transaction.title=Operation details +debian.transaction.to_install=The following additional packages ({no}) will be installed (Download: {dsize} | Installation: {isize}) +debian.transaction.to_remove=The following packages ({no}) will be removed (Freed space: {fspace}) +debian.uninstall.failed_to_remove=It was not possible to remove the following {no} packages: {pkgs} +debian.uninstall.removing=Removing packages +debian.uninstall.validating=Validating removed packages diff --git a/bauh/gems/debian/resources/locale/en b/bauh/gems/debian/resources/locale/en new file mode 100644 index 00000000..34c27357 --- /dev/null +++ b/bauh/gems/debian/resources/locale/en @@ -0,0 +1,84 @@ +debian.action.index_apps=Index applications +debian.action.index_apps.desc=Identifies all installed packages associated with .desktop files and maps them as applications +debian.action.purge=Remove completely +debian.action.purge.confirmation=The complete removal of {pkg} will erase the software files and also the configuration ones +debian.action.purge.desc=Removes all software and configuration files associated with the package +debian.action.purge.status=Removing {} completely +debian.action.sync_pkgs=Synchronize packages +debian.action.sync_pkgs.desc=Synchronizes the available software packages on the repositories +debian.action.sync_pkgs.status=Synchronizing packages +debian.action.sources=Software sources +debian.action.sources.desc=Starts the application for managing software sources +debian.action.sources.not_installed=The application for managing software sources is not installed +debian.action.sources.status=Launching application +debian.action.sources.unsupported=The application {app} is not supported and will not be started +debian.app_index.checking=Checking the apps index +debian.app_index.updating=Updating the apps index +debian.config.index_apps.exp=Apps cache expiration +debian.config.index_apps.exp.tip=Time period in MINUTES in which the installed applications cache is considered up-to-date during startup +debian.config.pkg_sources.app=Software sources +debian.config.pkg_sources.app.tip=Application for managing the software sources. {auto} detects one of the supported applications automatically. +debian.config.pkg_sources.app.auto=Auto +debian.config.suggestions.exp=Suggestions expiration +debian.config.suggestions.exp.tip=It defines the period (in hours) in which the suggestions stored in disc will be considered up to date. Use 0 if you always want to update them. +debian.config.sync_pkgs.time=Packages synchronization period +debian.config.sync_pkgs.time.tip=Time period in MINUTES in which the packages synchronization must be done on startup +debian.info.00.name=name +debian.info.01.version=version +debian.info.02.description=description +debian.info.03.exec=executable +debian.info.04.architecture=architecture +debian.info.04.architecture.all=all +debian.info.04.archive=archive +debian.info.04.automatically installed=automatically installed +debian.info.04.automatically installed.no=no +debian.info.04.automatically installed.yes=yes +debian.info.04.breaks=breaks +debian.info.04.compressed size=Download size +debian.info.04.conflicts=conflicts +debian.info.04.depends=dependencies +debian.info.04.filename=file +debian.info.04.homepage=web page +debian.info.04.maintainer=maintainer +debian.info.04.md5sum=MD5 +debian.info.04.multi-arch=multi-architecture +debian.info.04.multi-arch.foreign=foreign +debian.info.04.multi-arch.same=same +debian.info.04.replaces=replaces +debian.info.04.predepends=pre dependencies +debian.info.04.priority=priority +debian.info.04.priority.important=important +debian.info.04.priority.optional=optional +debian.info.04.provided by=provided by +debian.info.04.provides=provides +debian.info.04.recommends=recommends +debian.info.04.section=section +debian.info.04.state=state +debian.info.04.state.installed=installed +debian.info.04.state.not installed=not installed +debian.info.04.state.not installed (configuration files remain)=not installed (configuration files installed) +debian.info.04.suggests=suggests +debian.info.04.uncompressed size=installation size +debian.install.validating=Validating installed packages +debian.installing_pkgs=Installing packages +debian.output.downloading=Downloading {pkg} +debian.output.finishing=Finishing +debian.output.removing=Removing {pkg} +debian.output.unpacking=Unpacking {pkg} +debian.remove.impossible=It is not possible to remove the package {pkg} +debian.remove_deps=The following {no} dependent packages of {pkg} will be removed as well ({fspace}) +debian.simulate_operation=Simulating the operation +debian.task.app_index.status=Indexing installed applications +debian.task.map_apps.check_files=Checking {type} files +debian.task.map_apps.read_cache=Reading apps from the index (cache) +debian.task.map_apps.status=Mapping installed applications +debian.task.update_apps_idx.status=Updating index +debian.task.sync_pkgs.status=Synchronizing packages +debian.transaction.dependency_of=Dependency of {pkgs} +debian.transaction.get_data=Retrieving the operation's data +debian.transaction.title=Operation details +debian.transaction.to_install=The following additional packages ({no}) will be installed (Download: {dsize} | Installation: {isize}) +debian.transaction.to_remove=The following packages ({no}) will be removed (Freed space: {fspace}) +debian.uninstall.failed_to_remove=It was not possible to remove the following {no} packages: {pkgs} +debian.uninstall.removing=Removing packages +debian.uninstall.validating=Validating removed packages diff --git a/bauh/gems/debian/resources/locale/es b/bauh/gems/debian/resources/locale/es new file mode 100644 index 00000000..305e1bc0 --- /dev/null +++ b/bauh/gems/debian/resources/locale/es @@ -0,0 +1,84 @@ +debian.action.index_apps=Indexar aplicaciones +debian.action.index_apps.desc=Identifica todos los paquetes instalados asociados con archivos .desktop y los mapea como aplicaciones +debian.action.purge=Eliminar completamente +debian.action.purge.confirmation=La eliminación completa de {pkg} borrará los archivos de software y también los de configuración +debian.action.purge.desc=Elimina los archivos de software y configuración asociados al paquete +debian.action.purge.status=Eliminando {} completamente +debian.action.sync_pkgs=Sincronizar paquetes +debian.action.sync_pkgs.desc=Sincroniza los paquetes de software disponibles en los repositorios +debian.action.sync_pkgs.status=Sincronizando paquetes +debian.action.sources=Fuentes de software +debian.action.sources.desc=Inicia la aplicación para administrar fuentes de software +debian.action.sources.not_installed=La aplicación para administrar fuentes de software no está instalada +debian.action.sources.status=Iniciando la aplicación +debian.action.sources.unsupported=La aplicación {app} no es compatible y no se iniciará +debian.app_index.checking=Verificando el índice de applicationes +debian.app_index.updating=Actualizando el índice de aplicaciones +debian.config.index_apps.exp=Caducidad de caché de aplicaciones +debian.config.index_apps.exp.tip=Periodo de tiempo en MINUTOS en que el cache de las aplicaciones instaladas es considerado actualizado durante la inicialización +debian.config.pkg_sources.app=Fuentes de software +debian.config.pkg_sources.app.tip=Aplicación para administrar las fuentes de software. {auto} detecta una de las aplicaciones compatibles automáticamente. +debian.config.pkg_sources.app.auto=Auto +debian.config.suggestions.exp=Expiración de sugerencias +debian.config.suggestions.exp.tip=Define el período (en horas) en el que la sugerencias almacenadas en disco seran consideradas actualizadas. Use 0 si desea siempre actualizarlas. +debian.config.sync_pkgs.time=Período de sincronización de paquetes +debian.config.sync_pkgs.time.tip=Período de tiempo en MINUTOS en que se debe realizar la sincronización de paquetes durante la inicialización +debian.info.00.name=nombre +debian.info.01.version=versión +debian.info.02.description=descripción +debian.info.03.exec=ejecutable +debian.info.04.architecture=arquitectura +debian.info.04.architecture.all=todas +debian.info.04.archive=archivamiento +debian.info.04.automatically installed=instalado automáticamente +debian.info.04.automatically installed.no=no +debian.info.04.automatically installed.yes=si +debian.info.04.breaks=roturas +debian.info.04.compressed size=Tamaño de descarga +debian.info.04.conflicts=conflictos +debian.info.04.depends=dependencias +debian.info.04.filename=archivo +debian.info.04.homepage=página web +debian.info.04.maintainer=mantenedor +debian.info.04.md5sum=MD5 +debian.info.04.multi-arch=multi-arquitectura +debian.info.04.multi-arch.foreign=diferente +debian.info.04.multi-arch.same=misma +debian.info.04.replaces=reemplaza +debian.info.04.predepends=pre dependencias +debian.info.04.priority=prioridad +debian.info.04.priority.important=importante +debian.info.04.priority.opcional=opcional +debian.info.04.provided by=proporcionado por +debian.info.04.provides=proporciona +debian.info.04.recommends=recomienda +debian.info.04.section=sección +debian.info.04.state=estado +debian.info.04.state.installed=instalado +debian.info.04.state.not installed=no instalado +debian.info.04.state.not installed (configuration files remain)=no instalado (archivos de configuración instalados) +debian.info.04.suggests=sugiere +debian.info.04.uncompressed size=tamaño de instalación +debian.install.validating=Validando paquetes instalados +debian.installing_pkgs=Instalando paquetes +debian.output.downloading=Descargando {pkg} +debian.output.finishing=Finalizando +debian.output.removing=Eliminando {pkg} +debian.output.unpacking=Desempacando {pkg} +debian.remove.impossible=No es posible eliminar el paquete {pkg} +debian.remove_deps=Los siguientes {no} paquetes dependientes de {pkg} serán eliminados también ({fspace}) +debian.simulate_operation=Simulando la operación +debian.task.app_index.status=Indexando aplicaciones instaladas +debian.task.map_apps.check_files=Comprobando archivos {type} +debian.task.map_apps.read_cache=Leyendo aplicaciones del índice (cache) +debian.task.map_apps.status=Mapeando las aplicaciones instaladas +debian.task.update_apps_idx.status=Actualizando índice +debian.task.sync_pkgs.status=Sincronizando paquetes +debian.transaction.dependency_of=Dependencia de {pkgs} +debian.transaction.get_data=Recuperando datos de la operación +debian.transaction.title=Detalles de la operación +debian.transaction.to_install=Los siguientes paquetes adicionales ({no}) serán instalados (Descarga: {dsize} | Instalación: {isize}) +debian.transaction.to_remove=Los siguientes paquetes ({no}) serán eliminados (Espacio liberado: {fspace}) +debian.uninstall.failed_to_remove=No fue posible eliminar los siguientes {no} paquetes: {pkgs} +debian.uninstall.removing=Removendo paquetes +debian.uninstall.validating=Validando paquetes eliminados diff --git a/bauh/gems/debian/resources/locale/fr b/bauh/gems/debian/resources/locale/fr new file mode 100644 index 00000000..352b9660 --- /dev/null +++ b/bauh/gems/debian/resources/locale/fr @@ -0,0 +1,83 @@ +debian.action.index_apps=Index applications +debian.action.index_apps.desc=Identifies all installed packages associated with .desktop files and maps them as applications +debian.action.purge=Remove completely +debian.action.purge.confirmation=The complete removal of {pkg} will erase the software files and also the configuration ones +debian.action.purge.desc=Removes all software and configuration files associated with the package +debian.action.purge.status=Removing {} completely +debian.action.sync_pkgs=Synchronize packages +debian.action.sync_pkgs.desc=Synchronizes the available software packages on the repositories +debian.action.sync_pkgs.status=Synchronizing packages +debian.action.sources=Software sources +debian.action.sources.desc=Starts the application for managing software sources +debian.action.sources.not_installed=The application for managing software sources is not installed +debian.action.sources.status=Launching application +debian.action.sources.unsupported=The application {app} is not supported and will not be started +debian.app_index.checking=Checking the apps index +debian.app_index.updating=Updating the apps index +debian.config.index_apps.exp=Apps cache expiration +debian.config.index_apps.exp.tip=Time period in MINUTES in which the installed applications cache is considered up-to-date during startup +debian.config.pkg_sources.app=Software sources +debian.config.pkg_sources.app.tip=Application for managing the software sources. {auto} detects one of the supported applications automatically. +debian.config.pkg_sources.app.auto=Auto +debian.config.suggestions.exp=Suggestions expiration +debian.config.suggestions.exp.tip=It defines the period (in hours) in which the suggestions stored in disc will be considered up to date. Use 0 if you always want to update them. +debian.config.sync_pkgs.time=Packages synchronization period +debian.config.sync_pkgs.time.tip=Time period in MINUTES in which the packages synchronization must be done on startup +debian.info.00.name=name +debian.info.01.version=version +debian.info.02.description=description +debian.info.03.exec=executable +debian.info.04.architecture=architecture +debian.info.04.architecture.all=all +debian.info.04.archive=archive +debian.info.04.automatically installed=automatically installed +debian.info.04.automatically installed.no=no +debian.info.04.automatically installed.yes=yes +debian.info.04.breaks=breaks +debian.info.04.compressed size=Download size +debian.info.04.conflicts=conflicts +debian.info.04.depends=dependencies +debian.info.04.filename=file +debian.info.04.homepage=web page +debian.info.04.maintainer=maintainer +debian.info.04.md5sum=MD5 +debian.info.04.multi-arch=multi-architecture +debian.info.04.multi-arch.foreign=foreign +debian.info.04.multi-arch.same=same +debian.info.04.replaces=replaces +debian.info.04.predepends=pre dependencies +debian.info.04.priority=priority +debian.info.04.priority.important=important +debian.info.04.priority.optional=optional +debian.info.04.provided by=provided by +debian.info.04.provides=provides +debian.info.04.recommends=recommends +debian.info.04.section=section +debian.info.04.state=state +debian.info.04.state.installed=installed +debian.info.04.state.not installed=not installed +debian.info.04.state.not installed (configuration files remain)=not installed (configuration files installed) +debian.info.04.suggests=suggests +debian.info.04.uncompressed size=installation size +debian.install.validating=Validating installed packages +debian.output.downloading=Downloading {pkg} +debian.output.finishing=Finishing +debian.output.removing=Removing {pkg} +debian.output.unpacking=Unpacking {pkg} +debian.remove.impossible=It is not possible to remove the package {pkg} +debian.remove_deps=The following {no} dependent packages of {pkg} will be removed as well ({fspace}) +debian.simulate_operation=Simulating the operation +debian.task.app_index.status=Indexing installed applications +debian.task.map_apps.check_files=Checking {type} files +debian.task.map_apps.read_cache=Reading apps from the index (cache) +debian.task.map_apps.status=Mapping installed applications +debian.task.update_apps_idx.status=Updating index +debian.task.sync_pkgs.status=Synchronizing packages +debian.transaction.dependency_of=Dependency of {pkgs} +debian.transaction.get_data=Retrieving the operation's data +debian.transaction.title=Operation details +debian.transaction.to_install=The following additional packages ({no}) will be installed (Download: {dsize} | Installation: {isize}) +debian.transaction.to_remove=The following packages ({no}) will be removed (Freed space: {fspace}) +debian.uninstall.failed_to_remove=It was not possible to remove the following {no} packages: {pkgs} +debian.uninstall.removing=Removing packages +debian.uninstall.validating=Validating removed packages \ No newline at end of file diff --git a/bauh/gems/debian/resources/locale/it b/bauh/gems/debian/resources/locale/it new file mode 100644 index 00000000..41360778 --- /dev/null +++ b/bauh/gems/debian/resources/locale/it @@ -0,0 +1,84 @@ +debian.action.index_apps=Index applications +debian.action.index_apps.desc=Identifies all installed packages associated with .desktop files and maps them as applications +debian.action.purge=Remove completely +debian.action.purge.confirmation=The complete removal of {pkg} will erase the software files and also the configuration ones +debian.action.purge.desc=Removes all software and configuration files associated with the package +debian.action.purge.status=Removing {} completely +debian.action.sync_pkgs=Synchronize packages +debian.action.sync_pkgs.desc=Synchronizes the available software packages on the repositories +debian.action.sync_pkgs.status=Synchronizing packages +debian.action.sources=Software sources +debian.action.sources.desc=Starts the application for managing software sources +debian.action.sources.not_installed=The application for managing software sources is not installed +debian.action.sources.status=Launching application +debian.action.sources.unsupported=The application {app} is not supported and will not be started +debian.app_index.checking=Checking the apps index +debian.app_index.updating=Updating the apps index +debian.config.index_apps.exp=Apps cache expiration +debian.config.index_apps.exp.tip=Time period in MINUTES in which the installed applications cache is considered up-to-date during startup +debian.config.pkg_sources.app=Software sources +debian.config.pkg_sources.app.tip=Application for managing the software sources. {auto} detects one of the supported applications automatically. +debian.config.pkg_sources.app.auto=Auto +debian.config.suggestions.exp=Suggestions expiration +debian.config.suggestions.exp.tip=It defines the period (in hours) in which the suggestions stored in disc will be considered up to date. Use 0 if you always want to update them. +debian.config.sync_pkgs.time=Packages synchronization period +debian.config.sync_pkgs.time.tip=Time period in MINUTES in which the packages synchronization must be done on startup +debian.info.00.name=name +debian.info.01.version=version +debian.info.02.description=description +debian.info.03.exec=executable +debian.info.04.architecture=architecture +debian.info.04.architecture.all=all +debian.info.04.archive=archive +debian.info.04.automatically installed=automatically installed +debian.info.04.automatically installed.no=no +debian.info.04.automatically installed.yes=yes +debian.info.04.breaks=breaks +debian.info.04.compressed size=Download size +debian.info.04.conflicts=conflicts +debian.info.04.depends=dependencies +debian.info.04.filename=file +debian.info.04.homepage=web page +debian.info.04.maintainer=maintainer +debian.info.04.md5sum=MD5 +debian.info.04.multi-arch=multi-architecture +debian.info.04.multi-arch.foreign=foreign +debian.info.04.multi-arch.same=same +debian.info.04.replaces=replaces +debian.info.04.predepends=pre dependencies +debian.info.04.priority=priority +debian.info.04.priority.important=important +debian.info.04.priority.optional=optional +debian.info.04.provided by=provided by +debian.info.04.provides=provides +debian.info.04.recommends=recommends +debian.info.04.section=section +debian.info.04.state=state +debian.info.04.state.installed=installed +debian.info.04.state.not installed=not installed +debian.info.04.state.not installed (configuration files remain)=not installed (configuration files installed) +debian.info.04.suggests=suggests +debian.info.04.uncompressed size=installation size +debian.install.validating=Validating installed packages +debian.installing_pkgs=Installing packages +debian.output.downloading=Downloading {pkg} +debian.output.finishing=Finishing +debian.output.removing=Removing {pkg} +debian.output.unpacking=Unpacking {pkg} +debian.remove.impossible=It is not possible to remove the package {pkg} +debian.remove_deps=The following {no} dependent packages of {pkg} will be removed as well ({fspace}) +debian.simulate_operation=Simulating the operation +debian.task.app_index.status=Indexing installed applications +debian.task.map_apps.check_files=Checking {type} files +debian.task.map_apps.read_cache=Reading apps from the index (cache) +debian.task.map_apps.status=Mapping installed applications +debian.task.update_apps_idx.status=Updating index +debian.task.sync_pkgs.status=Synchronizing packages +debian.transaction.dependency_of=Dependency of {pkgs} +debian.transaction.get_data=Retrieving the operation's data +debian.transaction.title=Operation details +debian.transaction.to_install=The following additional packages ({no}) will be installed (Download: {dsize} | Installation: {isize}) +debian.transaction.to_remove=The following packages ({no}) will be removed (Freed space: {fspace}) +debian.uninstall.failed_to_remove=It was not possible to remove the following {no} packages: {pkgs} +debian.uninstall.removing=Removing packages +debian.uninstall.validating=Validating removed packages \ No newline at end of file diff --git a/bauh/gems/debian/resources/locale/pt b/bauh/gems/debian/resources/locale/pt new file mode 100644 index 00000000..a4de0efd --- /dev/null +++ b/bauh/gems/debian/resources/locale/pt @@ -0,0 +1,84 @@ +debian.action.index_apps=Indexar aplicações +debian.action.index_apps.desc=Identifica todos os pacotes instalados associados a arquivos .desktop e os mapeia como aplicações +debian.action.purge=Remover completamente +debian.action.purge.confirmation=A remoção completa de {pkg} apagará os arquivos de software e também os de configuração +debian.action.purge.desc=Remove todos os arquivos de software e configuração associados ao pacote +debian.action.purge.status=Removendo {} completamente +debian.action.sync_pkgs=Sincronizar pacotes +debian.action.sync_pkgs.desc=Sincroniza os pacotes de software disponíveis nos repositórios +debian.action.sync_pkgs.status=Sincronizando pacotes +debian.action.sources=Fontes de software +debian.action.sources.desc=Inicia a aplicação detectada para gerenciamento de fontes de software +debian.action.sources.not_installed=A aplicação para gerenciamento de fontes de software não está instalada +debian.action.sources.status=Iniciando aplicação +debian.action.sources.unsupported=A aplicação {app} não é suportada e não será inicializada +debian.app_index.checking=Veriricando o índice de aplicações +debian.app_index.updating=Atualizando o índice de aplicações +debian.config.index_apps.exp=Validade do cache de aplicações +debian.config.index_apps.exp.tip=Período de tempo em MINUTOS em que o cache das aplicações instaladas é considerado válido durante a inicialização +debian.config.pkg_sources.app=Fontes de software +debian.config.pkg_sources.app.tip=Aplicação para gerenciamento de fontes de software. {auto} detecta uma das aplicações suportadas automaticamente. +debian.config.pkg_sources.app.auto=Auto +debian.config.suggestions.exp=Validade de sugestões +debian.config.suggestions.exp.tip=Define o período (em horas) no qual as sugestões armazenadas em disco serão consideradas atualizadas. Use 0 se quiser que elas sempre sejam atualizadas. +debian.config.sync_pkgs.time=Período de sincronização de pacotes +debian.config.sync_pkgs.time.tip=Período de tempo em MINUTOS em que a sincronização de pacotes deve ocorrer durante a inicialização +debian.info.00.name=nome +debian.info.01.version=versão +debian.info.02.description=descrição +debian.info.03.exec=executável +debian.info.04.architecture=arquitetura +debian.info.04.architecture.all=todas +debian.info.04.archive=arquivamento +debian.info.04.automatically installed=instalado automaticamente +debian.info.04.automatically installed.no=não +debian.info.04.automatically installed.yes=sim +debian.info.04.breaks=Quebra +debian.info.04.compressed size=Tamanho de download +debian.info.04.conflicts=Conflita +debian.info.04.depends=dependências +debian.info.04.filename=arquivo +debian.info.04.homepage=página web +debian.info.04.maintainer=mantenedor +debian.info.04.md5sum=MD5 +debian.info.04.multi-arch=multi-arquitetura +debian.info.04.multi-arch.foreign=diferente +debian.info.04.multi-arch.same=mesma +debian.info.04.replaces=substitui +debian.info.04.predepends=pré-dependências +debian.info.04.priority=prioridade +debian.info.04.priority.important=importante +debian.info.04.priority.optional=opcional +debian.info.04.provided by=provido por +debian.info.04.provides=provê +debian.info.04.recommends=recomenda +debian.info.04.section=seção +debian.info.04.state=estado +debian.info.04.state.installed=instalado +debian.info.04.state.not installed=não instalado +debian.info.04.state.not installed (configuration files remain)=não instalado (arquivos de configuração instalados) +debian.info.04.suggests=sugere +debian.info.04.uncompressed size=tamanho de instalação +debian.install.validating=Validando pacotes instalados +debian.installing_pkgs=Instalando pacotes +debian.output.downloading=Baixando {pkg} +debian.output.finishing=Finalizando +debian.output.removing=Removendo {pkg} +debian.output.unpacking=Desempacotando {pkg} +debian.remove.impossible=Não é possível remover o pacote {pkg} +debian.remove_deps=Os seguintes {no} pacotes dependentes de {pkg} serão removidos também ({fspace}) +debian.simulate_operation=Simulando a operação +debian.task.app_index.status=Indexando aplicações instaladas +debian.task.map_apps.check_files=Verificando arquivos {type} +debian.task.map_apps.read_cache=Lendo aplicações do índice (cache) +debian.task.map_apps.status=Mapeando aplicações instaladas +debian.task.update_apps_idx.status=Atualizando índice +debian.task.sync_pkgs.status=Sincronizando pacotes +debian.transaction.dependency_of=Dependência de {pkgs} +debian.transaction.get_data=Obtendo dados da operação +debian.transaction.title=Detalhes da operação +debian.transaction.to_install=Os seguintes pacotes adicionais ({no}) serão instaladas (Download: {dsize} | Instalação: {isize}) +debian.transaction.to_remove=Os seguintes pacotes ({no}) serão removidos (Espaço liberado: {fspace}) +debian.uninstall.failed_to_remove=Não foi possível remover os seguintes {no} pacotes: {pkgs} +debian.uninstall.removing=Removendo pacotes +debian.uninstall.validating=Validando pacotes removidos diff --git a/bauh/gems/debian/resources/locale/ru b/bauh/gems/debian/resources/locale/ru new file mode 100644 index 00000000..41360778 --- /dev/null +++ b/bauh/gems/debian/resources/locale/ru @@ -0,0 +1,84 @@ +debian.action.index_apps=Index applications +debian.action.index_apps.desc=Identifies all installed packages associated with .desktop files and maps them as applications +debian.action.purge=Remove completely +debian.action.purge.confirmation=The complete removal of {pkg} will erase the software files and also the configuration ones +debian.action.purge.desc=Removes all software and configuration files associated with the package +debian.action.purge.status=Removing {} completely +debian.action.sync_pkgs=Synchronize packages +debian.action.sync_pkgs.desc=Synchronizes the available software packages on the repositories +debian.action.sync_pkgs.status=Synchronizing packages +debian.action.sources=Software sources +debian.action.sources.desc=Starts the application for managing software sources +debian.action.sources.not_installed=The application for managing software sources is not installed +debian.action.sources.status=Launching application +debian.action.sources.unsupported=The application {app} is not supported and will not be started +debian.app_index.checking=Checking the apps index +debian.app_index.updating=Updating the apps index +debian.config.index_apps.exp=Apps cache expiration +debian.config.index_apps.exp.tip=Time period in MINUTES in which the installed applications cache is considered up-to-date during startup +debian.config.pkg_sources.app=Software sources +debian.config.pkg_sources.app.tip=Application for managing the software sources. {auto} detects one of the supported applications automatically. +debian.config.pkg_sources.app.auto=Auto +debian.config.suggestions.exp=Suggestions expiration +debian.config.suggestions.exp.tip=It defines the period (in hours) in which the suggestions stored in disc will be considered up to date. Use 0 if you always want to update them. +debian.config.sync_pkgs.time=Packages synchronization period +debian.config.sync_pkgs.time.tip=Time period in MINUTES in which the packages synchronization must be done on startup +debian.info.00.name=name +debian.info.01.version=version +debian.info.02.description=description +debian.info.03.exec=executable +debian.info.04.architecture=architecture +debian.info.04.architecture.all=all +debian.info.04.archive=archive +debian.info.04.automatically installed=automatically installed +debian.info.04.automatically installed.no=no +debian.info.04.automatically installed.yes=yes +debian.info.04.breaks=breaks +debian.info.04.compressed size=Download size +debian.info.04.conflicts=conflicts +debian.info.04.depends=dependencies +debian.info.04.filename=file +debian.info.04.homepage=web page +debian.info.04.maintainer=maintainer +debian.info.04.md5sum=MD5 +debian.info.04.multi-arch=multi-architecture +debian.info.04.multi-arch.foreign=foreign +debian.info.04.multi-arch.same=same +debian.info.04.replaces=replaces +debian.info.04.predepends=pre dependencies +debian.info.04.priority=priority +debian.info.04.priority.important=important +debian.info.04.priority.optional=optional +debian.info.04.provided by=provided by +debian.info.04.provides=provides +debian.info.04.recommends=recommends +debian.info.04.section=section +debian.info.04.state=state +debian.info.04.state.installed=installed +debian.info.04.state.not installed=not installed +debian.info.04.state.not installed (configuration files remain)=not installed (configuration files installed) +debian.info.04.suggests=suggests +debian.info.04.uncompressed size=installation size +debian.install.validating=Validating installed packages +debian.installing_pkgs=Installing packages +debian.output.downloading=Downloading {pkg} +debian.output.finishing=Finishing +debian.output.removing=Removing {pkg} +debian.output.unpacking=Unpacking {pkg} +debian.remove.impossible=It is not possible to remove the package {pkg} +debian.remove_deps=The following {no} dependent packages of {pkg} will be removed as well ({fspace}) +debian.simulate_operation=Simulating the operation +debian.task.app_index.status=Indexing installed applications +debian.task.map_apps.check_files=Checking {type} files +debian.task.map_apps.read_cache=Reading apps from the index (cache) +debian.task.map_apps.status=Mapping installed applications +debian.task.update_apps_idx.status=Updating index +debian.task.sync_pkgs.status=Synchronizing packages +debian.transaction.dependency_of=Dependency of {pkgs} +debian.transaction.get_data=Retrieving the operation's data +debian.transaction.title=Operation details +debian.transaction.to_install=The following additional packages ({no}) will be installed (Download: {dsize} | Installation: {isize}) +debian.transaction.to_remove=The following packages ({no}) will be removed (Freed space: {fspace}) +debian.uninstall.failed_to_remove=It was not possible to remove the following {no} packages: {pkgs} +debian.uninstall.removing=Removing packages +debian.uninstall.validating=Validating removed packages \ No newline at end of file diff --git a/bauh/gems/debian/resources/locale/tr b/bauh/gems/debian/resources/locale/tr new file mode 100644 index 00000000..b9458348 --- /dev/null +++ b/bauh/gems/debian/resources/locale/tr @@ -0,0 +1,83 @@ +debian.action.index_apps=Index applications +debian.action.index_apps.desc=Identifies all installed packages associated with .desktop files and maps them as applications +debian.action.purge=Remove completely +debian.action.purge.confirmation=The complete removal of {pkg} will erase the software files and also the configuration ones +debian.action.purge.desc=Removes all software and configuration files associated with the package +debian.action.purge.status=Removing {} completely +debian.action.sync_pkgs=Synchronize packages +debian.action.sync_pkgs.desc=Synchronizes the available software packages on the repositories +debian.action.sync_pkgs.status=Synchronizing packages +debian.action.sources=Software sources +debian.action.sources.desc=Starts the application for managing software sources +debian.action.sources.not_installed=The application for managing software sources is not installed +debian.action.sources.status=Launching application +debian.action.sources.unsupported=The application {app} is not supported and will not be started +debian.app_index.checking=Checking the apps index +debian.app_index.updating=Updating the apps index +debian.config.index_apps.exp=Apps cache expiration +debian.config.index_apps.exp.tip=Time period in MINUTES in which the installed applications cache is considered up-to-date during startup +debian.config.pkg_sources.app=Software sources +debian.config.pkg_sources.app.tip=Application for managing the software sources. {auto} detects one of the supported applications automatically. +debian.config.pkg_sources.app.auto=Auto +debian.config.suggestions.exp=Suggestions expiration +debian.config.suggestions.exp.tip=It defines the period (in hours) in which the suggestions stored in disc will be considered up to date. Use 0 if you always want to update them. +debian.config.sync_pkgs.time=Packages synchronization period +debian.config.sync_pkgs.time.tip=Time period in MINUTES in which the packages synchronization must be done on startup +debian.info.00.name=name +debian.info.01.version=version +debian.info.02.description=description +debian.info.03.exec=executable +debian.info.04.architecture=architecture +debian.info.04.architecture.all=all +debian.info.04.archive=archive +debian.info.04.automatically installed=automatically installed +debian.info.04.automatically installed.no=no +debian.info.04.automatically installed.yes=yes +debian.info.04.breaks=breaks +debian.info.04.compressed size=Download size +debian.info.04.conflicts=conflicts +debian.info.04.depends=dependencies +debian.info.04.homepage=web page +debian.info.04.maintainer=maintainer +debian.info.04.md5sum=MD5 +debian.info.04.multi-arch=multi-architecture +debian.info.04.multi-arch.foreign=foreign +debian.info.04.multi-arch.same=same +debian.info.04.replaces=replaces +debian.info.04.predepends=pre dependencies +debian.info.04.priority=priority +debian.info.04.priority.important=important +debian.info.04.priority.optional=optional +debian.info.04.provided by=provided by +debian.info.04.provides=provides +debian.info.04.recommends=recommends +debian.info.04.section=section +debian.info.04.state=state +debian.info.04.state.installed=installed +debian.info.04.state.not installed=not installed +debian.info.04.state.not installed (configuration files remain)=not installed (configuration files installed) +debian.info.04.suggests=suggests +debian.info.04.uncompressed size=installation size +debian.install.validating=Validating installed packages +debian.installing_pkgs=Installing packages +debian.output.downloading=Downloading {pkg} +debian.output.finishing=Finishing +debian.output.removing=Removing {pkg} +debian.output.unpacking=Unpacking {pkg} +debian.remove.impossible=It is not possible to remove the package {pkg} +debian.remove_deps=The following {no} dependent packages of {pkg} will be removed as well ({fspace}) +debian.simulate_operation=Simulating the operation +debian.task.app_index.status=Indexing installed applications +debian.task.map_apps.check_files=Checking {type} files +debian.task.map_apps.read_cache=Reading apps from the index (cache) +debian.task.map_apps.status=Mapping installed applications +debian.task.update_apps_idx.status=Updating index +debian.task.sync_pkgs.status=Synchronizing packages +debian.transaction.dependency_of=Dependency of {pkgs} +debian.transaction.get_data=Retrieving the operation's data +debian.transaction.title=Operation details +debian.transaction.to_install=The following additional packages ({no}) will be installed (Download: {dsize} | Installation: {isize}) +debian.transaction.to_remove=The following packages ({no}) will be removed (Freed space: {fspace}) +debian.uninstall.failed_to_remove=It was not possible to remove the following {no} packages: {pkgs} +debian.uninstall.removing=Removing packages +debian.uninstall.validating=Validating removed packages \ No newline at end of file diff --git a/bauh/gems/debian/suggestions.py b/bauh/gems/debian/suggestions.py new file mode 100644 index 00000000..da361ebf --- /dev/null +++ b/bauh/gems/debian/suggestions.py @@ -0,0 +1,192 @@ +import os +import traceback +from datetime import datetime, timedelta +from logging import Logger +from pathlib import Path +from threading import Thread +from typing import Optional, Dict + +from bauh.api.abstract.handler import TaskManager +from bauh.api.abstract.model import SuggestionPriority +from bauh.api.http import HttpClient +from bauh.gems.debian import DEBIAN_ICON_PATH, DEBIAN_CACHE_DIR +from bauh.view.util.translation import I18n + + +class DebianSuggestionsDownloader(Thread): + + _file_suggestions: Optional[str] = None + _file_suggestions_ts: Optional[str] = None + _url_suggestions: Optional[str] = None + + @classmethod + def file_suggestions(cls) -> str: + if cls._file_suggestions is None: + cls._file_suggestions = f'{DEBIAN_CACHE_DIR}/suggestions.txt' + + return cls._file_suggestions + + @classmethod + def file_suggestions_timestamp(cls) -> str: + if cls._file_suggestions_ts is None: + cls._file_suggestions_ts = f'{cls.file_suggestions()}.ts' + + return cls._file_suggestions_ts + + @classmethod + def url_suggestions(cls) -> str: + if cls._url_suggestions is None: + cls._url_suggestions = 'https://raw.githubusercontent.com/vinifmor/bauh-files' \ + '/master/debian/suggestions.txt' + + return cls._url_suggestions + + def __init__(self, logger: Logger, http_client: HttpClient, i18n: I18n): + super(DebianSuggestionsDownloader, self).__init__() + self._log = logger + self.i18n = i18n + self.http_client = http_client + self._taskman: Optional[TaskManager] = None + self.task_id = 'debian.suggs' + + def register_task(self, taskman: Optional[TaskManager]): + self._taskman = taskman + if taskman: + self._taskman.register_task(id_=self.task_id, label=self.i18n['task.download_suggestions'], + icon_path=DEBIAN_ICON_PATH) + + @property + def taskman(self) -> TaskManager: + if self._taskman is None: + self._taskman = TaskManager() + + return self._taskman + + @classmethod + def should_download(cls, debian_config: dict, logger: Logger, only_positive_exp: bool = False) -> bool: + try: + exp_hours = int(debian_config['suggestions.exp']) + except ValueError: + logger.error(f"The Debian configuration property 'suggestions.expiration' has a non int value set: " + f"{debian_config['suggestions']['expiration']}") + return not only_positive_exp + + if exp_hours <= 0: + logger.info("Suggestions cache is disabled") + return not only_positive_exp + + if not os.path.exists(cls.file_suggestions()): + logger.info(f"'{cls.file_suggestions()}' not found. It must be downloaded") + return True + + if not os.path.exists(cls.file_suggestions()): + logger.info(f"'{cls.file_suggestions()}' not found. The suggestions file must be downloaded.") + return True + + with open(cls.file_suggestions_timestamp()) as f: + timestamp_str = f.read() + + try: + suggestions_timestamp = datetime.fromtimestamp(float(timestamp_str)) + except: + logger.error(f'Could not parse the Debian cached suggestions timestamp: {timestamp_str}') + traceback.print_exc() + return True + + update = suggestions_timestamp + timedelta(hours=exp_hours) <= datetime.utcnow() + return update + + def _save(self, text: str, timestamp: float): + self._log.info(f"Caching suggestions to '{self.file_suggestions()}'") + + cache_dir = os.path.dirname(self.file_suggestions()) + + try: + Path(cache_dir).mkdir(parents=True, exist_ok=True) + cache_dir_ok = True + except OSError: + self._log.error(f"Could not create cache directory '{cache_dir}'") + traceback.print_exc() + cache_dir_ok = False + + if cache_dir_ok: + try: + with open(self.file_suggestions(), 'w+') as f: + f.write(text) + except: + self._log.error(f"An exception happened while writing the file '{self.file_suggestions()}'") + traceback.print_exc() + + try: + with open(self.file_suggestions_timestamp(), 'w+') as f: + f.write(str(timestamp)) + except: + self._log.error(f"An exception happened while writing the file '{self.file_suggestions_timestamp()}'") + traceback.print_exc() + + def parse_suggestions(self, suggestions_str: str) -> Dict[str, SuggestionPriority]: + output = dict() + for line in suggestions_str.split('\n'): + clean_line = line.strip() + + if clean_line: + line_split = clean_line.split(':', 1) + + if len(line_split) == 2: + try: + prio = int(line_split[0]) + except ValueError: + self._log.warning(f"Could not parse Debian package suggestion: {line}") + continue + + output[line_split[1]] = SuggestionPriority(prio) + + return output + + def read_cached(self) -> Optional[Dict[str, SuggestionPriority]]: + self._log.info(f"Reading cached suggestions file '{self.file_suggestions()}'") + + try: + with open(self.file_suggestions()) as f: + sugs_str = f.read() + except FileNotFoundError: + self._log.warning(f"Cached suggestions file does not exist ({self.file_suggestions()})") + return + + if not sugs_str: + self._log.warning(f"Cached suggestions file '{self.file_suggestions()}' is empty") + return + + return self.parse_suggestions(sugs_str) + + def download(self) -> Optional[Dict[str, SuggestionPriority]]: + self.taskman.update_progress(self.task_id, progress=1, substatus=None) + + self._log.info(f"Downloading suggestions from {self.url_suggestions()}") + res = self.http_client.get(self.url_suggestions()) + + suggestions = None + if res.status_code == 200 and res.text: + self.taskman.update_progress(self.task_id, progress=50, substatus=None) + suggestions = self.parse_suggestions(res.text) + + if suggestions: + self._save(text=res.text, timestamp=datetime.utcnow().timestamp()) + else: + self._log.warning("No Debian suggestions to cache") + else: + self._log.warning(f"Could not retrieve Debian suggestions. " + f"Response (status={res.status_code}, text={res.text})") + + self.taskman.update_progress(self.task_id, progress=100, substatus=None) + self.taskman.finish_task(self.task_id) + return suggestions + + def read(self, debian_config: dict) -> Optional[Dict[str, int]]: + if self.should_download(debian_config=debian_config, logger=self._log): + return self.download() + + return self.read_cached() + + def run(self): + self.download() diff --git a/bauh/gems/debian/tasks.py b/bauh/gems/debian/tasks.py new file mode 100644 index 00000000..51a18730 --- /dev/null +++ b/bauh/gems/debian/tasks.py @@ -0,0 +1,196 @@ +import time +import traceback +from datetime import datetime, timedelta +from logging import Logger +from threading import Thread +from typing import Optional, Set + +from bauh.api.abstract.handler import TaskManager, ProcessWatcher +from bauh.commons.html import bold +from bauh.commons.system import ProcessHandler +from bauh.gems.debian import DEBIAN_ICON_PATH, PACKAGE_SYNC_TIMESTAMP_FILE +from bauh.gems.debian.aptitude import Aptitude +from bauh.gems.debian.index import ApplicationIndexer, ApplicationsMapper +from bauh.gems.debian.model import DebianApplication +from bauh.view.util.translation import I18n + + +class MapApplications(Thread): + + def __init__(self, taskman: TaskManager, app_indexer: ApplicationIndexer, i18n: I18n, logger: Logger, + deb_config: dict, app_mapper: ApplicationsMapper, check_expiration: bool = True, + watcher: Optional[ProcessWatcher] = None): + super(MapApplications, self).__init__() + self._taskman = taskman + self._i18n = i18n + self._log = logger + self._id = 'debian.map_apps' + self._indexer = app_indexer + self._config = deb_config + self._app_mapper = app_mapper + self._taskman.register_task(self._id, self._i18n['debian.task.map_apps.status'], DEBIAN_ICON_PATH) + self.apps: Optional[Set[DebianApplication]] = None + self.cached: Optional[bool] = None + self._check_expiration = check_expiration + self._watcher = watcher + + def _change_substatus(self, msg: str): + if self._watcher: + self._watcher.change_substatus(msg) + + def run(self): + ti = time.time() + self._log.info("Begin: mapping Debian applications") + + self._taskman.update_progress(self._id, 1, None) + + if not self._check_expiration or self._indexer.is_expired(self._config): + status = self._i18n['debian.task.map_apps.check_files'].format(type="'.desktop") + self._taskman.update_progress(self._id, 1, status) + self._change_substatus(status) + self.apps = self._app_mapper.map_executable_applications() + self.cached = False + else: + status = self._i18n['debian.task.map_apps.read_cache'] + self._taskman.update_progress(self._id, 1, status) + self._change_substatus(status) + self.apps = {app for app in self._indexer.read_index()} + self.cached = True + + self._log.info(f"Number of Debian applications found: {len(self.apps) if self.apps else 0}") + self._taskman.update_progress(self._id, 100, None) + self._taskman.finish_task(self._id) + + tf = time.time() + self._log.info(f"Finish: mapping Debian applications ({tf - ti:.4f} seconds)") + + +class UpdateApplicationIndex(Thread): + + def __init__(self, taskman: TaskManager, app_indexer: ApplicationIndexer, + i18n: I18n, logger: Logger, mapping_apps: MapApplications, + watcher: Optional[ProcessWatcher] = None): + super(UpdateApplicationIndex, self).__init__() + self._taskman = taskman + self._i18n = i18n + self._log = logger + self._id = 'debian.app_idx' + self._indexer = app_indexer + self._mapping_apps = mapping_apps + self._taskman.register_task(self._id, self._i18n['debian.task.app_index.status'], DEBIAN_ICON_PATH) + self._watcher = watcher + + def _change_substatus(self, msg: str): + if self._watcher: + self._watcher.change_substatus(msg) + + def run(self): + ti = time.time() + self._log.info("Begin: Debian applications indexation") + + self._taskman.update_progress(self._id, 1, self._i18n['task.waiting_task'].format(bold(self._i18n['debian.task.map_apps.status']))) + self._mapping_apps.join() + + finish_msg = None + if self._mapping_apps.cached: + finish_msg = self._i18n['task.canceled'] + else: + status = self._i18n['debian.task.update_apps_idx.status'] + self._taskman.update_progress(self._id, 50, status) + self._change_substatus(status) + + try: + self._indexer.update_index(self._mapping_apps.apps) + except: + finish_msg = self._i18n['error'] + + self._taskman.update_progress(self._id, 100, finish_msg) + self._taskman.finish_task(self._id) + + tf = time.time() + self._log.info(f"Finish: Debian applications indexation ({tf - ti:.4f} seconds)") + + +class SynchronizePackages(Thread): + + def __init__(self, taskman: TaskManager, i18n: I18n, logger: Logger, root_password: Optional[str], + aptitude: Aptitude, watcher: Optional[ProcessWatcher] = None): + super(SynchronizePackages, self).__init__() + self._id = 'debian.sync_pkgs' + self._taskman = taskman + self._i18n = i18n + self._log = logger + self._root_password = root_password + self._watcher = watcher + self._aptitude = aptitude + self._taskman.register_task(self._id, self._i18n['debian.task.sync_pkgs.status'], DEBIAN_ICON_PATH) + + def _notify_output(self, output: str): + self._taskman.update_output(self._id, output) + + @staticmethod + def should_synchronize(deb_config: dict, logger: Logger) -> bool: + try: + period = int(deb_config.get('sync_pkgs.time', 0)) + except ValueError: + logger.error(f"Invalid value for Debian configuration property 'sync_pkgs.time': " + f"{deb_config['sync_pkgs.time']}") + return True + + if period <= 0: + logger.warning("Packages synchronization will always be done ('sync_pkgs.time' <= 0 )'") + return True + + try: + with open(PACKAGE_SYNC_TIMESTAMP_FILE) as f: + timestamp_str = f.read().strip() + except FileNotFoundError: + logger.info(f"No packages synchronization timestamp found ({PACKAGE_SYNC_TIMESTAMP_FILE})") + return True + + try: + last_timestamp = datetime.fromtimestamp(float(timestamp_str)) + except: + logger.error(f'Could not parse the packages synchronization timestamp: {timestamp_str} ' + f'({PACKAGE_SYNC_TIMESTAMP_FILE})') + traceback.print_exc() + return True + + expired = last_timestamp + timedelta(minutes=period) <= datetime.utcnow() + + if expired: + logger.info("Packages synchronization is outdated") + else: + logger.info("Packages synchronization is up-to-date") + + return expired + + def run(self) -> bool: + ti = time.time() + self._log.info("Begin: packages synchronization") + self._taskman.update_progress(self._id, 1, None) + + handler = ProcessHandler(self._watcher) + updated, _ = handler.handle_simple(self._aptitude.update(self._root_password), + output_handler=self._notify_output) + self._taskman.update_progress(self._id, 99, None) + + if updated: + index_timestamp = datetime.utcnow().timestamp() + finish_msg = None + try: + with open(PACKAGE_SYNC_TIMESTAMP_FILE, 'w+') as f: + f.write(str(index_timestamp)) + except OSError: + finish_msg = self._i18n['error'] + self._log.error(f"Could not write the packages synchronization timestamp to file " + f"'{PACKAGE_SYNC_TIMESTAMP_FILE}'") + else: + finish_msg = self._i18n['error'] + + self._taskman.update_progress(self._id, 100, finish_msg) + self._taskman.finish_task(self._id) + + tf = time.time() + self._log.info(f"Finish: packages synchronization ({tf - ti:.4f} seconds)") + return updated diff --git a/bauh/gems/web/resources/locale/ca b/bauh/gems/web/resources/locale/ca index 0bb0b7b7..4271b195 100644 --- a/bauh/gems/web/resources/locale/ca +++ b/bauh/gems/web/resources/locale/ca @@ -81,7 +81,6 @@ web.settings.suggestions.cache_exp=Suggestions expiration web.settings.suggestions.cache_exp.tip=It defines the period (in HOURS) in which suggestions stored on the disk are considered up to date during the initialization process. Use 0 so that they are always updated web.task.download_settings=Updating environment settings web.task.search_index=Indexing suggestions -web.task.suggestions=Downloading suggestions web.task.suggestions.saving=Saving to disk web.uninstall.error.install_dir.not_found=The installation directory {} was not found web.uninstall.error.remove_dir=It was not possible to remove {} diff --git a/bauh/gems/web/resources/locale/en b/bauh/gems/web/resources/locale/en index 64012bc9..07b5b1e6 100644 --- a/bauh/gems/web/resources/locale/en +++ b/bauh/gems/web/resources/locale/en @@ -81,7 +81,6 @@ web.settings.suggestions.cache_exp=Suggestions expiration web.settings.suggestions.cache_exp.tip=It defines the period (in HOURS) in which suggestions stored on the disk are considered up to date during the initialization process. Use 0 so that they are always updated. web.task.download_settings=Updating environment settings web.task.search_index=Indexing suggestions -web.task.suggestions=Downloading suggestions web.task.suggestions.saving=Saving to disk web.uninstall.error.install_dir.not_found=The installation directory {} was not found web.uninstall.error.remove_dir=It was not possible to remove {} diff --git a/bauh/gems/web/resources/locale/es b/bauh/gems/web/resources/locale/es index 0844df57..ba7987d9 100644 --- a/bauh/gems/web/resources/locale/es +++ b/bauh/gems/web/resources/locale/es @@ -81,7 +81,6 @@ web.settings.suggestions.cache_exp=Expiración de sugerencias web.settings.suggestions.cache_exp.tip=Define el período (en HORAS) en el que las sugerencias almacenadas en el disco son consideradas actualizadas durante el proceso de inicialización. Utilice 0 para que siempre sean actualizadas. web.task.download_settings=Actualizando configuraciones de ambiente web.task.search_index=Indexando sugerencias -web.task.suggestions=Descargando sugerencias web.task.suggestions.saving=Guardando en disco web.uninstall.error.install_dir.not_found=No se encontró el directorio de instalación {} web.uninstall.error.remove_dir=No fue posible eliminar {} diff --git a/bauh/gems/web/resources/locale/fr b/bauh/gems/web/resources/locale/fr index edc29679..83afe698 100644 --- a/bauh/gems/web/resources/locale/fr +++ b/bauh/gems/web/resources/locale/fr @@ -81,7 +81,6 @@ web.settings.suggestions.cache_exp=Suggestions expiration web.settings.suggestions.cache_exp.tip=It defines the period (in HOURS) in which suggestions stored on the disk are considered up to date during the initialization process. Use 0 so that they are always updated web.task.download_settings=Mise à jour des paramètres d'environnement web.task.search_index=Indexing suggestions -web.task.suggestions=Téléchargement de suggestions web.task.suggestions.saving=Saving to disk web.uninstall.error.install_dir.not_found=Impossible de trouver le dossier d'installation {} web.uninstall.error.remove_dir=Impossible d'enlever {} diff --git a/bauh/gems/web/resources/locale/it b/bauh/gems/web/resources/locale/it index 92451abb..6bf797a3 100644 --- a/bauh/gems/web/resources/locale/it +++ b/bauh/gems/web/resources/locale/it @@ -81,7 +81,6 @@ web.settings.suggestions.cache_exp=Suggestions expiration web.settings.suggestions.cache_exp.tip=It defines the period (in HOURS) in which suggestions stored on the disk are considered up to date during the initialization process. Use 0 so that they are always updated web.task.download_settings=Updating environment settings web.task.search_index=Indexing suggestions -web.task.suggestions=Downloading suggestions web.task.suggestions.saving=Saving to disk web.uninstall.error.install_dir.not_found=The installation directory {} was not found web.uninstall.error.remove_dir=It was not possible to remove {} diff --git a/bauh/gems/web/resources/locale/pt b/bauh/gems/web/resources/locale/pt index 62f02b45..008853aa 100644 --- a/bauh/gems/web/resources/locale/pt +++ b/bauh/gems/web/resources/locale/pt @@ -80,7 +80,6 @@ web.settings.suggestions.cache_exp=Expiração de sugestões web.settings.suggestions.cache_exp.tip=Define o período (em HORAS) em que as sugestões armazenadas em disco são consideradas atualizadas durante a inicialização. Use 0 para que elas sejam sempre atualizadas. web.task.download_settings=Atualizando configurações de ambiente web.task.search_index=Indexando sugestões -web.task.suggestions=Baixando sugestões web.task.suggestions.saving=Salvando em disco web.uninstall.error.install_dir.not_found=O diretório de instalação {} não foi encontrado web.uninstall.error.remove=Não foi possível remover {} diff --git a/bauh/gems/web/resources/locale/ru b/bauh/gems/web/resources/locale/ru index 8b1ae1b4..4a8668f3 100644 --- a/bauh/gems/web/resources/locale/ru +++ b/bauh/gems/web/resources/locale/ru @@ -81,7 +81,6 @@ web.settings.suggestions.cache_exp=Suggestions expiration web.settings.suggestions.cache_exp.tip=It defines the period (in HOURS) in which suggestions stored on the disk are considered up to date during the initialization process. Use 0 so that they are always updated web.task.download_settings=Updating environment settings web.task.search_index=Indexing suggestions -web.task.suggestions=Downloading suggestions web.task.suggestions.saving=Saving to disk web.uninstall.error.install_dir.not_found=Каталог установки {} не найден web.uninstall.error.remove_dir=Не удалось удалить {} diff --git a/bauh/gems/web/resources/locale/tr b/bauh/gems/web/resources/locale/tr index 8c2db1f4..a605aaf2 100644 --- a/bauh/gems/web/resources/locale/tr +++ b/bauh/gems/web/resources/locale/tr @@ -81,7 +81,6 @@ web.settings.suggestions.cache_exp=Suggestions expiration web.settings.suggestions.cache_exp.tip=It defines the period (in HOURS) in which suggestions stored on the disk are considered up to date during the initialization process. Use 0 so that they are always updated web.task.download_settings=Ortam ayarları güncelleniyor web.task.search_index=Indexing suggestions -web.task.suggestions=Downloading suggestions web.task.suggestions.saving=Saving to disk web.uninstall.error.install_dir.not_found={} kurulum dizini bulunamadı web.uninstall.error.remove_dir={} kaldırmak mümkün olmadı diff --git a/bauh/gems/web/worker.py b/bauh/gems/web/worker.py index 953dce59..bcff6488 100644 --- a/bauh/gems/web/worker.py +++ b/bauh/gems/web/worker.py @@ -30,7 +30,7 @@ class SuggestionsLoader(Thread): self.suggestions = suggestions self.create_config = create_config self.internet_connection = internet_connection - self.task_name = self.i18n['web.task.suggestions'] + self.task_name = self.i18n['task.download_suggestions'] self.taskman.register_task(self.task_id, self.task_name, get_icon_path()) def run(self): diff --git a/bauh/manage.py b/bauh/manage.py index 2a4ea28b..29e582a5 100644 --- a/bauh/manage.py +++ b/bauh/manage.py @@ -56,21 +56,24 @@ def new_manage_panel(app_args: Namespace, app_config: dict, logger: logging.Logg app = new_qt_application(app_config=app_config, logger=logger, quit_on_last_closed=True) + screen_size = app.primaryScreen().size() + context.screen_width, context.screen_height = screen_size.width(), screen_size.height() + if app_args.settings: # only settings window manager.cache_available_managers() - return app, SettingsWindow(manager=manager, i18n=i18n, screen_size=app.primaryScreen().size(), window=None) + return app, SettingsWindow(manager=manager, i18n=i18n, screen_size=screen_size, window=None) else: manage_window = ManageWindow(i18n=i18n, manager=manager, icon_cache=icon_cache, - screen_size=app.primaryScreen().size(), + screen_size=screen_size, config=app_config, context=context, http_client=http_client, icon=util.get_default_icon()[1], logger=logger) - prepare = PreparePanel(screen_size=app.primaryScreen().size(), + prepare = PreparePanel(screen_size=screen_size, context=context, manager=manager, i18n=i18n, diff --git a/bauh/view/core/controller.py b/bauh/view/core/controller.py index 07089cd1..6c88db27 100755 --- a/bauh/view/core/controller.py +++ b/bauh/view/core/controller.py @@ -163,7 +163,7 @@ class GenericSoftwareManager(SoftwareManager): 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, is_url=is_url) + 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') @@ -650,6 +650,7 @@ class GenericSoftwareManager(SoftwareManager): for man in working_managers: for action in man.gen_custom_actions(): + action.manager = man yield action app_config = self.configman.get_config() diff --git a/bauh/view/qt/components.py b/bauh/view/qt/components.py index 43a23c24..a3fbdd23 100644 --- a/bauh/view/qt/components.py +++ b/bauh/view/qt/components.py @@ -996,6 +996,12 @@ def to_widget(comp: ViewComponent, i18n: I18n, parent: QWidget = None) -> QWidge elif isinstance(comp, TextComponent): label = QLabel(comp.value) + if comp.min_width is not None and comp.min_width > 0: + label.setMinimumWidth(comp.min_width) + + if comp.max_width is not None and comp.max_width > 0: + label.setMinimumWidth(comp.max_width) + if comp.size is not None: label.setStyleSheet("QLabel { font-size: " + str(comp.size) + "px }") diff --git a/bauh/view/qt/info.py b/bauh/view/qt/info.py index a3a1a56a..d16757de 100644 --- a/bauh/view/qt/info.py +++ b/bauh/view/qt/info.py @@ -1,3 +1,5 @@ +from collections.abc import Iterable + from PyQt5.QtCore import QSize, Qt from PyQt5.QtGui import QIcon, QCursor from PyQt5.QtWidgets import QDialog, QVBoxLayout, QGroupBox, \ @@ -53,8 +55,9 @@ class InfoDialog(QDialog): for idx, attr in enumerate(sorted(pkg_info.keys())): if attr not in IGNORED_ATTRS and pkg_info[attr]: i18n_key = pkg_info['__app__'].model.gem_name + '.info.' + attr.lower() + val = pkg_info[attr] - if isinstance(pkg_info[attr], list): + if not isinstance(val, str) and isinstance(pkg_info[attr], Iterable): val = ' '.join([str(e).strip() for e in pkg_info[attr] if e]) show_val = '\n'.join(['* ' + str(e).strip() for e in pkg_info[attr] if e]) else: diff --git a/bauh/view/qt/thread.py b/bauh/view/qt/thread.py index b411b48b..b510b6b4 100644 --- a/bauh/view/qt/thread.py +++ b/bauh/view/qt/thread.py @@ -529,8 +529,9 @@ class UpgradeSelected(AsyncAction): self.change_substatus('') if success: - updated = len(requirements.to_upgrade) - updated_types.update((req.pkg.__class__ for req in requirements.to_upgrade)) + if requirements.to_upgrade: + updated = len(requirements.to_upgrade) + updated_types.update((req.pkg.__class__ for req in requirements.to_upgrade)) if should_trim: self._trim_disk(root_password) diff --git a/bauh/view/resources/locale/ca b/bauh/view/resources/locale/ca index 7e95e8cf..a6705553 100644 --- a/bauh/view/resources/locale/ca +++ b/bauh/view/resources/locale/ca @@ -112,22 +112,32 @@ cancel=cancel·la categories=categories category=categoria category.2dgraphics=gràfics 2d +category.acessories=acessories +category.admin=admin category.administration=administració category.amusement=diversió +category.application=application category.arcadegame=joc «arcade» +category.archiving=archiving category.art=art category.audio=àudio category.audiovideo=àudio/vídeo category.books=llibres category.browser=navegador +category.calculator=calculator category.cloud=núvol category.communication=comunicació +category.compression=compression +category.core=core category.database=base de dades category.databases=bases de dades category.design=disseny category.desktopsettings=paràmetres de l’escriptori +category.devel=desenvolupament category.development=desenvolupament category.devices=dispositius +category.dictionary=dictionary +category.doc=documentation category.education=educació category.emulator=emulador category.entertainment=entreteniment @@ -140,12 +150,17 @@ category.games=jocs category.graphics=gràfics category.health=salut category.instantmessaging=missatgeria +category.libs=biblioteca category.library=biblioteca +category.localization=localization +category.math=math category.media=mitjans category.messaging=missatgeria +category.monitor=monitoring category.movie=pel·lícula category.movies=pel·lícules category.music=música +category.net=xarxa category.network=xarxa category.networks=xarxes category.news=notícies @@ -154,7 +169,9 @@ category.office=oficina category.packagemanager=gestor de paquets category.personalisation=personalització category.photo=foto +category.photography=foto category.player=jugador +category.presentation=presentation category.productivity=productivitat category.reference=referència category.science=ciència @@ -162,6 +179,7 @@ category.security=seguretat category.server=servidor category.settings=configuració category.social=social +category.sound=sound category.sportsgame=joc d’esport category.system=sistema category.terminalemulator=terminal @@ -176,6 +194,7 @@ category.videoeditor=editor de vídeo category.viewer=visor category.weather=el temps category.web=web +category.webbrowser=navegador category.webdevelopment=desenvolupament web change=modifica clean=netejar @@ -362,6 +381,7 @@ notification.update_selected.success=aplicacions actualitzades correctament ok=ok others=altres popup.button.cancel=Cancel·la +popup.button.continue=Continuar popup.button.no=No popup.button.yes=Sí popup.history.selected.tooltip=Versió actual @@ -372,6 +392,8 @@ popup.root.continue=Continuar popup.root.msg=Requireix la vostra contrasenya per a continuar popup.root.title=Autenticació popup.screenshots.no_screenshot.body=no s’han pogut recuperar captures de pantalla del {} +popup.title.error=Error +popup.title.warning=Avis prepare.bt_hide_details=Hide details prepare.bt_icon.no_output=There are no progress details for this task to be displayed prepare.bt_icon.output=Click here to display the progress details of this task @@ -398,9 +420,11 @@ status.caching_data=S’estan emmagatzemant {} dades a la memòria cau al disc style=estil success=success summary=resum +task.canceled=canceled task.checking_config=Checking configuration file task.checking_config.saving=Creating file task.download_categories=Download de categories +task.download_suggestions=Downloading de suggeriments task.waiting_task=Waiting for {} type=tipus uninstall=desinstal·la diff --git a/bauh/view/resources/locale/de b/bauh/view/resources/locale/de index 12c94b42..b1f09a84 100644 --- a/bauh/view/resources/locale/de +++ b/bauh/view/resources/locale/de @@ -112,21 +112,31 @@ cancel=Abbrechen categories=Kategorien category=Kategorie category.2dgraphics=2D-Grafik +category.acessories=acessories +category.admin=admin category.administration=Verwaltung category.amusement=amusement +category.application=application category.arcadegame=Arcade-Spiel +category.archiving=archiving category.art=art category.audio=Audio category.audiovideo=Audio/Video category.books=books category.browser=browser +category.calculator=calculator category.cloud=cloud category.communication=Kommunikation +category.compression=compression +category.core=core category.database=database category.databases=databases category.design=design category.desktopsettings=Desktop-Einstellungen +category.devel=Entwicklung category.development=Entwicklung +category.dictionary=dictionary +category.doc=documentation category.education=education category.emulator=Emulator category.entertainment=entertainment @@ -139,12 +149,17 @@ category.games=games category.graphics=graphics category.health=health category.instantmessaging=Kommunikation +category.libs=library category.library=library +category.localization=localization +category.math=math category.media=media category.messaging=messaging +category.monitor=monitoring category.movie=movie category.movies=movies category.music=music +category.net=network category.network=network category.networks=networks category.news=news @@ -153,7 +168,9 @@ category.office=office category.packagemanager=Paketmanager category.personalisation=personalization category.photo=photo +category.photography=photo category.player=Spieler +category.presentation=presentation category.productivity=productivity category.reference=reference category.science=science @@ -161,6 +178,7 @@ category.security=security category.server=server category.settings=Einstellungen category.social=social +category.sound=sound category.sportsgame=Sportspiel category.system=system category.terminalemulator=Terminal @@ -175,6 +193,7 @@ category.videoeditor=video editor category.viewer=viewer category.weather=weather category.web=web +category.webbrowser=browser category.webdevelopment=Web-Entwicklung change=Anwenden clean=Bereinigen @@ -361,6 +380,7 @@ notification.update_selected.success=Anwendung(en) erfolgreich aktualisiert ok=ok others=Weitere popup.button.cancel=Abbrechen +popup.button.continue=Fortsetzen popup.button.no=Nein popup.button.yes=Ja popup.history.selected.tooltip=Aktuelle Version @@ -371,6 +391,8 @@ popup.root.continue=Fortfahren popup.root.msg=Zum Fortfahren wird das Passwort benötigt. popup.root.title=Authentifizierung popup.screenshots.no_screenshot.body={} Screenshots konnten nicht heruntergeladen werden. +popup.title.error=Fehler +popup.title.warning=Warnung prepare.bt_hide_details=Hide details prepare.bt_icon.no_output=There are no progress details for this task to be displayed prepare.bt_icon.output=Click here to display the progress details of this task @@ -398,9 +420,11 @@ status.caching_data={} Daten auf der Festplatte zwischenspeichern style=Stil success=success summary=Zusammenfassung +task.canceled=canceled task.checking_config=Checking configuration file task.checking_config.saving=Creating file task.download_categories=Downloading categories +task.download_suggestions=Downloading suggestions task.waiting_task=Waiting for {} type=Typ uninstall=Deinstallation diff --git a/bauh/view/resources/locale/en b/bauh/view/resources/locale/en index 1d54a14c..5dfcc963 100644 --- a/bauh/view/resources/locale/en +++ b/bauh/view/resources/locale/en @@ -111,21 +111,31 @@ bt.not_now=Not now cancel=cancel categories=categories category.2dgraphics=2d graphics +category.acessories=acessories +category.admin=admin category.administration=administration category.amusement=amusement +category.application=application category.arcadegame=arcade game +category.archiving=archiving category.art=art category.audio=audio category.audiovideo=audio / video category.books=books category.browser=browser +category.calculator=calculator category.cloud=cloud category.communication=communication +category.compression=compression +category.core=core category.database=database category.databases=databases category.design=design category.desktopsettings=desktop settings +category.devel=development category.development=development +category.dictionary=dictionary +category.doc=documentation category.education=education category.emulator=emulator category.entertainment=entertainment @@ -136,14 +146,21 @@ category.fonts=fonts category.game=game category.games=games category.graphics=graphics +category.hardwaresettings=hardware category.health=health category.instantmessaging=messaging +category.libs=library category.library=library +category.localization=localization +category.math=math +category.mail=e-mail category.media=media category.messaging=messaging +category.monitor=monitoring category.movie=movie category.movies=movies category.music=music +category.net=network category.network=network category.networks=networks category.news=news @@ -151,8 +168,10 @@ category.none=None category.office=office category.packagemanager=package manager category.personalisation=personalization -category.photo=photo +category.photo=photography +category.photography=photography category.player=player +category.presentation=presentation category.productivity=productivity category.reference=reference category.science=science @@ -160,6 +179,7 @@ category.security=security category.server=server category.settings=settings category.social=social +category.sound=sound category.sportsgame=sports game category.system=system category.terminalemulator=terminal @@ -174,6 +194,7 @@ category.videoeditor=video editor category.viewer=viewer category.weather=weather category.web=web +category.webbrowser=browser category.webdevelopment=web development category=category change=change @@ -362,6 +383,7 @@ notification.update_selected.success=app(s) updated successfully ok=ok others=others popup.button.cancel=Cancel +popup.button.continue=Continue popup.button.no=No popup.button.yes=Yes popup.history.selected.tooltip=Current version @@ -372,6 +394,8 @@ popup.root.continue=Continue popup.root.msg=Requires your password to continue popup.root.title=Authentication popup.screenshots.no_screenshot.body=it was not possible to retrieve {} screenshots +popup.title.error=Error +popup.title.warning=Warning prepare.bt_hide_details=Hide details prepare.bt_icon.no_output=There are no progress details for this task to be displayed prepare.bt_icon.output=Click here to display the progress details of this task @@ -399,9 +423,11 @@ status.caching_data=Caching {} data to disk style=style success=success summary=summary +task.canceled=canceled task.checking_config=Checking configuration file task.checking_config.saving=Creating file task.download_categories=Downloading categories +task.download_suggestions=Downloading suggestions task.waiting_task=Waiting for {} type=type uninstall=uninstall diff --git a/bauh/view/resources/locale/es b/bauh/view/resources/locale/es index f29725d8..53cb55f9 100644 --- a/bauh/view/resources/locale/es +++ b/bauh/view/resources/locale/es @@ -112,22 +112,32 @@ cancel=cancelar categories=categorías category=categoría category.2dgraphics=gráficos 2d +category.acessories=accesorios +category.admin=administrador category.administration=administración category.amusement=diversión +category.application=aplicación category.applications=aplicaciones category.arcadegame=juego de máquina +category.archiving=archivo category.art=arte category.audio=audio category.audiovideo=audio y vídeo category.books=libros category.browser=navegador +category.calculator=calculadora category.cloud=nube category.communication=comunicación +category.compression=compresión +category.core=núcleo category.database=base de datos category.databases=bases de datos category.design=diseño category.desktopsettings=configuraciones +category.devel=desarrollo category.development=desarrollo +category.dictionary=diccionario +category.doc=documentación category.education=educación category.emulator=emulador category.entertainment=entretenimiento @@ -140,12 +150,17 @@ category.games=juegos category.graphics=gráficos category.health=salud category.instantmessaging=mensajería +category.libs=biblioteca category.library=biblioteca +category.localization=localización +category.math=matemáticas category.media=media category.messaging=mensajería +category.monitor=monitoreo category.movie=película category.movies=películas category.music=música +category.net=red category.network=red category.networks=redes category.news=noticias @@ -153,8 +168,10 @@ category.none=ninguna category.office=oficina category.packagemanager=administrador de paquetes category.personalisation=personalización -category.photo=foto +category.photo=fotografía +category.photography=fotografía category.player=jugador +category.presentation=presentación category.productivity=productividad category.reference=referencia category.science=ciencias @@ -162,6 +179,7 @@ category.security=seguridad category.server=servidor category.settings=configuraciones category.social=social +category.sound=sonido category.sportsgame=juego de deportes category.system=sistema category.terminalemulator=terminal @@ -176,6 +194,7 @@ category.videoeditor=editor de vídeos category.viewer=viewer category.weather=tiempo category.web=web +category.webbrowser=navegador category.webdevelopment=desarrollo web change=cambiar clean=limpiar @@ -363,6 +382,7 @@ notification.update_selected.success=aplicación(es) actualizada(s) correctament ok=ok others=otros popup.button.cancel=Cancelar +popup.button.continue=Continuar popup.button.no=No popup.button.yes=Sí popup.history.selected.tooltip=Versión actual @@ -373,6 +393,8 @@ popup.root.continue=Continuar popup.root.msg=Proporcione su contraseña para continuar popup.root.title=Autenticación popup.screenshots.no_screenshot.body=No fue posible recuperar las fotos de {} +popup.title.error=Error +popup.title.warning=Aviso prepare.bt_hide_details=Ocultar detalles prepare.bt_icon.no_output=No hay detalles del progreso de esta tarea para mostrar prepare.bt_icon.output=Haga clic aquí para mostrar los detalles del progreso de esta tarea @@ -401,9 +423,11 @@ status.caching_data=Almacenando en antememoria los datos de {} para el disco style=estilo success=éxito summary=resumen +task.canceled=cancelada task.checking_config=Verificando archivo de configuración task.checking_config.saving=Creando archivo task.download_categories=Descargando categorías +task.download_suggestions=Descargando sugerencias task.waiting_task=Esperando por {} type=tipo uninstall=desinstalar diff --git a/bauh/view/resources/locale/fr b/bauh/view/resources/locale/fr index c8a95bef..3e59d1da 100644 --- a/bauh/view/resources/locale/fr +++ b/bauh/view/resources/locale/fr @@ -111,21 +111,31 @@ bt.not_now=Pas maintenant cancel=Annuler categories=categories category.2dgraphics=graphisqme 2d +category.acessories=acessories +category.admin=admin category.administration=administration category.amusement=amusement +category.application=application category.arcadegame=jeu d'arcade +category.archiving=archiving category.art=art category.audio=son category.audiovideo=son / video category.books=livres category.browser=navigateur +category.calculator=calculator category.cloud=cloud category.communication=communication +category.compression=compression +category.core=core category.database=base de données category.databases=bases de données category.design=design category.desktopsettings=configuration +category.devel=développement category.development=développement +category.dictionary=dictionary +category.doc=documentation category.education=éducation category.emulator=émulateur category.entertainment=divertissement @@ -138,12 +148,17 @@ category.games=jeux category.graphics=graphiques category.health=santé category.instantmessaging=messagerie instantanée +category.libs=bibliothèque category.library=bibliothèque +category.localization=localization +category.math=math category.media=media category.messaging=messagerie +category.monitor=monitoring category.movie=film category.movies=films category.music=musique +category.net=réseau category.network=réseau category.networks=réseaux category.news=presse @@ -152,7 +167,9 @@ category.office=bureautique category.packagemanager=gestionnaire de paquets category.personalisation=personalisation category.photo=photo +category.photography=photo category.player=lecteur +category.presentation=presentation category.productivity=productivité category.reference=reference category.science=science @@ -160,6 +177,7 @@ category.security=securité category.server=serveur category.settings=paramères category.social=social +category.sound=sound category.sportsgame=jeu de sports category.system=système category.terminalemulator=terminal @@ -174,6 +192,7 @@ category.videoeditor=éditeur vidéo category.viewer=viewer category.weather=météo category.web=web +category.webbrowser=navigateur category.webdevelopment=développement web category=categorie change=changement @@ -358,6 +377,7 @@ notification.update_selected.success=app(s) mises à jour avec succès ok=ok others=autres popup.button.cancel=Annuler +popup.button.continue=Continuer popup.button.no=Non popup.button.yes=Oui popup.history.selected.tooltip=Version Actuelle @@ -368,6 +388,8 @@ popup.root.bad_password.title=Erreur popup.root.continue=continuer popup.root.title=Nécessite votre mot de passe pour continuer popup.screenshots.no_screenshot.body=impossible de récupérer les captures d'écran de {} +popup.title.error=Erreur +popup.title.warning=Avertissement prepare.bt_hide_details=Cacher les details prepare.bt_icon.no_output=Pas de progrès à afficher pour cette tâche prepare.bt_icon.output=Cliquez ici pour afficher l'avancement de cette tâche @@ -395,9 +417,11 @@ status.caching_data=Mise en cache des données de {} sur disque style=style success=succès summary=résumé +task.canceled=canceled task.checking_config=Checking configuration file task.checking_config.saving=Creating file task.download_categories=Téléchargement des catégories +task.download_suggestions=Téléchargement des suggestions task.waiting_task=Waiting for {} type=type uninstall=désinstaller diff --git a/bauh/view/resources/locale/it b/bauh/view/resources/locale/it index 039ce1b1..9480b5db 100644 --- a/bauh/view/resources/locale/it +++ b/bauh/view/resources/locale/it @@ -112,21 +112,31 @@ cancel=cancella categories=categorie category=categoria category.2dgraphics=Grafica 2D +category.acessories=acessories +category.admin=admin category.administration=amministrazione category.amusement=amusement +category.application=application category.arcadegame=Gioco arcade +category.archiving=archiving category.art=art category.audio=audio category.audiovideo=audio / video category.books=books category.browser=browser +category.calculator=calculator category.cloud=cloud category.communication=comunicazione +category.compression=compression +category.core=core category.database=database category.databases=databases category.design=design category.desktopsettings=impostazioni del desktop +category.devel=development category.development=development +category.dictionary=dictionary +category.doc=documentation category.education=education category.emulator=emulatore category.entertainment=entertainment @@ -139,12 +149,17 @@ category.games=games category.graphics=graphics category.health=health category.instantmessaging=messaggistica +category.libs=library category.library=library +category.localization=localization +category.math=math category.media=media category.messaging=messaggistica +category.monitor=monitoring category.movie=movie category.movies=movies category.music=music +category.net=network category.network=network category.networks=networks category.news=news @@ -153,7 +168,9 @@ category.office=office category.packagemanager=gestore pacchetti category.personalisation=personalization category.photo=photo +category.photography=photo category.player=giocatore +category.presentation=presentation category.productivity=productivity category.reference=reference category.science=science @@ -161,6 +178,7 @@ category.security=security category.server=server category.settings=impostazioni category.social=social +category.sound=sound category.sportsgame=gioco di sport category.system=system category.terminalemulator=terminale @@ -175,6 +193,7 @@ category.videoeditor=video editor category.viewer=viewer category.weather=weather category.web=web +category.webbrowser=browser category.webdevelopment=sviluppo web change=Cambia clean=pulire @@ -363,6 +382,7 @@ notification.update_selected.success=app aggiornate correttamente ok=ok others=altre popup.button.cancel=Cancella +popup.button.continue=Continuare popup.button.no=No popup.button.yes=Si popup.history.selected.tooltip=Version corrente @@ -373,6 +393,8 @@ popup.root.continue=Continuare popup.root.msg=Richiede la tua password per continuare popup.root.title=Autenticazione popup.screenshots.no_screenshot.body=non è stato possibile recuperare {} schermate +popup.title.error=Errore +popup.title.warning=Avviso prepar_panel.bt_skip.label=saltare prepare.bt_hide_details=Hide details prepare.bt_icon.no_output=There are no progress details for this task to be displayed @@ -401,9 +423,11 @@ status.caching_data=Memorizza {} dati sul disco style=stile success=success summary=riepilogo +task.canceled=canceled task.checking_config=Checking configuration file task.checking_config.saving=Creating file task.download_categories=Download delle categorie +task.download_suggestions=Download dei suggerimenti task.waiting_task=Waiting for {} type=tipe uninstall=Disinstalla diff --git a/bauh/view/resources/locale/pt b/bauh/view/resources/locale/pt index 89ec530a..5e156621 100644 --- a/bauh/view/resources/locale/pt +++ b/bauh/view/resources/locale/pt @@ -111,21 +111,31 @@ bt.not_now=Agora não cancel=cancelar categories=categorias category.2dgraphics=gŕaficos 2d +category.acessories=acessórios +category.admin=administrador category.administration=administração category.amusement=diversão +category.application=aplicação category.arcadegame=jogo arcade +category.archiving=arquivamento category.art=arte category.audio=áudio category.audiovideo=áudio / vídeo category.books=livros category.browser=navegador +category.calculator=calculadora category.cloud=nuvem category.communication=comunicação +category.compression=compressão +category.core=núcleo category.database=banco de dados category.databases=bancos de dados category.design=desenho category.desktopsettings=configurações +category.devel=desenvolvimento category.development=desenvolvimento +category.dictionary=dicionário +category.doc=documentação category.education=educação category.emulator=emulador category.entertainment=entretenimento @@ -138,12 +148,17 @@ category.games=jogos category.graphics=gráficos category.health=saúde category.instantmessaging=mensagem +category.libs=biblioteca category.library=biblioteca +category.localization=localização +category.math=matemática category.media=mídia category.messaging=mensagem +category.monitor=monitoria category.movie=filme category.movies=filmes category.music=música +category.net=rede category.network=rede category.networks=redes category.news=notícias @@ -151,8 +166,10 @@ category.none=Nenhuma category.office=escritório category.packagemanager=gerenciador de pacotes category.personalisation=personalização -category.photo=foto +category.photo=fotografia +category.photography=fotografia category.player=jogador +category.presentation=apresentação category.productivity=produtividade category.reference=referência category.science=ciências @@ -160,6 +177,7 @@ category.security=segurança category.server=servidor category.settings=configuração category.social=social +category.sound=som category.sportsgame=jogo de esportes category.system=sistema category.terminalemulator=terminal @@ -174,6 +192,7 @@ category.videoeditor=edito de vídeo category.viewer=visualizador category.weather=tempo category.web=web +category.webbrowser=navegador category.webdevelopment=desenvolvimento web category=categoria change=alterar @@ -362,6 +381,7 @@ notification.update_selected.success=aplicativo(s) atualizado(s) com sucesso ok=ok others=outros popup.button.cancel=Cancelar +popup.button.continue=Continuar popup.button.no=Não popup.button.yes=Sim popup.history.selected.tooltip=Versão atual @@ -372,6 +392,8 @@ popup.root.continue=Prosseguir popup.root.msg=Requer sua senha para prosseguir popup.root.title=Autenticação popup.screenshots.no_screenshot.body=Não foi possível obter as fotos de {} +popup.title.error=Erro +popup.title.warning=Aviso prepare.bt_hide_details=Ocultar detalhes prepare.bt_icon.no_output=Não existe detalhes do progresso dessa tarefa para ser exibido prepare.bt_icon.output=Clique aqui para exibir detalhes do progresso dessa tarefa @@ -399,9 +421,11 @@ status.caching_data=Cacheando dados de {} para o disco style=estilo success=sucesso summary=resumo +task.canceled=canceleda task.checking_config=Verificando arquivo de configuração task.checking_config.saving=Criando arquivo task.download_categories=Baixando categorias +task.download_suggestions=Baixando sugestões task.waiting_task=Aguardando {} type=tipo uninstall=desinstalar diff --git a/bauh/view/resources/locale/ru b/bauh/view/resources/locale/ru index 8ac7182a..d429a3dc 100644 --- a/bauh/view/resources/locale/ru +++ b/bauh/view/resources/locale/ru @@ -112,21 +112,31 @@ cancel=Отмена categories=Категории category=Категория category.2dgraphics=2D-графика +category.acessories=acessories +category.admin=admin category.administration=Администрирование category.amusement=amusement +category.application=application category.arcadegame=Аркада +category.archiving=archiving category.art=art category.audio=Аудио category.audiovideo=Аудио/Видео category.books=books category.browser=browser +category.calculator=calculator category.cloud=cloud category.communication=Общение +category.compression=compression +category.core=core category.database=database category.databases=databases category.design=design category.desktopsettings=Настройки рабочего стола +category.devel=Разработка category.development=Разработка +category.dictionary=dictionary +category.doc=documentation category.education=education category.emulator=Эмуляторы category.entertainment=entertainment @@ -139,12 +149,17 @@ category.games=games category.graphics=graphics category.health=health category.instantmessaging=Мессенджер +category.libs=library category.library=library +category.localization=localization +category.math=math category.media=media category.messaging=Сообщения +category.monitor=monitoring category.movie=movie category.movies=movies category.music=music +category.net=network category.network=network category.networks=networks category.news=news @@ -153,7 +168,9 @@ category.office=Офис category.packagemanager=Пакетный менеджер category.personalisation=personalization category.photo=photo +category.photography=photo category.player=Проигрыватель +category.presentation=presentation category.productivity=productivity category.reference=reference category.science=science @@ -161,6 +178,7 @@ category.security=security category.server=server category.settings=Настройки category.social=social +category.sound=sound category.sportsgame=Спортивная игра category.system=system category.terminalemulator=Эмулятор терминала @@ -175,6 +193,7 @@ category.videoeditor=video editor category.viewer=viewer category.weather=weather category.web=Сеть +category.webbrowser=browser category.webdevelopment=Web-разработка change=Изменить clean=Очистка @@ -361,6 +380,7 @@ notification.update_selected.success=Приложение(я) успешно о ok=ok others=Другие popup.button.cancel=Отмена +popup.button.continue=Продолжить popup.button.no=Нет popup.button.yes=Да popup.history.selected.tooltip=Текущая версия @@ -371,6 +391,8 @@ popup.root.continue=Продолжить popup.root.msg=Требуется ваш пароль, чтобы продолжить popup.root.title=Аутентификация popup.screenshots.no_screenshot.body=не удалось получить скриншоты {} +popup.title.error=Ошибка +popup.title.warning=Предупреждение prepare.bt_hide_details=Hide details prepare.bt_icon.no_output=There are no progress details for this task to be displayed prepare.bt_icon.output=Click here to display the progress details of this task @@ -398,9 +420,11 @@ status.caching_data=Кэширование данных {} на диск style=Стиль success=success summary=Суммарно +task.canceled=canceled task.checking_config=Checking configuration file task.checking_config.saving=Creating file task.download_categories=Downloading categories +task.download_suggestions=Downloading suggestions task.waiting_task=Waiting for {} type=Тип uninstall=Деинсталляция diff --git a/bauh/view/resources/locale/tr b/bauh/view/resources/locale/tr index 32c1d0a2..1c74f674 100644 --- a/bauh/view/resources/locale/tr +++ b/bauh/view/resources/locale/tr @@ -111,21 +111,31 @@ bt.not_now=Şimdi değil cancel=vazgeç categories=Kategoriler category.2dgraphics=2d grafikler +category.acessories=acessories +category.admin=admin category.administration=yönetim category.amusement=eğlence +category.application=application category.arcadegame=arcade oyunlar +category.archiving=archiving category.art=sanat category.audio=ses category.audiovideo=ses / video category.books=kitaplar category.browser=tarayıcı +category.calculator=calculator category.cloud=bulut category.communication=iletişim +category.compression=compression +category.core=core category.database=veritabanı category.databases=veritabanları category.design=tasarım category.desktopsettings=masaüstü ayarları +category.devel=gelişim category.development=gelişim +category.dictionary=dictionary +category.doc=documentation category.education=eğitim category.emulator=emülatör category.entertainment=eğlence @@ -138,12 +148,17 @@ category.games=oyunlar category.graphics=grafikler category.health=sağlık category.instantmessaging=mesajlaşma +category.libs=kütüphane category.library=kütüphane +category.localization=localization +category.math=math category.media=medya category.messaging=mesajlaşma +category.monitor=monitoring category.movie=film category.movies=filmler category.music=müzik +category.net=ağ category.network=ağ category.networks=ağlar category.news=haberler @@ -152,7 +167,9 @@ category.office=ofis category.packagemanager=paket yönetici category.personalisation=kişiselleştirme category.photo=fotoğraf +category.photography=fotoğraf category.player=oyuncu +category.presentation=presentation category.productivity=verimlilik category.reference=referans category.science=bilim @@ -160,6 +177,7 @@ category.security=güvenlik category.server=sunucu category.settings=ayarlar category.social=sosyal +category.sound=sound category.sportsgame=spor oyunları category.system=sistem category.terminalemulator=uçbirim @@ -174,6 +192,7 @@ category.videoeditor=video düzenleyici category.viewer=gösterici category.weather=hava durumu category.web=web +category.webbrowser=tarayıcı category.webdevelopment=web gelişimi category=kategori change=değiştir @@ -361,6 +380,7 @@ notification.update_selected.success=uygulama(lar) başarıyla güncellendi ok=tamam others=diğerleri popup.button.cancel=Vazgeç +popup.button.continue=Devam popup.button.no=Hayır popup.button.yes=Evet popup.history.selected.tooltip=Mevcut sürüm @@ -371,6 +391,8 @@ popup.root.continue=devam popup.root.msg=Devam etmek için şifre girmelisiniz popup.root.title=Doğrulama popup.screenshots.no_screenshot.body={} ekran görüntüsünü almak mümkün değildi +popup.title.error=Hata +popup.title.warning=Uyarı prepare.bt_hide_details=Hide details prepare.bt_icon.no_output=There are no progress details for this task to be displayed prepare.bt_icon.output=Click here to display the progress details of this task @@ -398,9 +420,11 @@ status.caching_data={} verilerini diske önbellekle style=Görünüm biçimi success=başarılı summary=özet +task.canceled=canceled task.checking_config=Checking configuration file task.checking_config.saving=Creating file task.download_categories=Kategoriler indiriliyor +task.download_suggestions=Downloading suggestions task.waiting_task=Waiting for {} type=tür uninstall=kaldır diff --git a/tests/__init__.py b/tests/__init__.py index e69de29b..17431ac7 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,10 @@ + +class AnyInstance(object): + def __init__(self, instance_class: type): + self._class = instance_class + + def __eq__(self, other): + return other is not None and isinstance(other, self._class) + + def __repr__(self): + return f'' diff --git a/tests/gems/debian/__init__.py b/tests/gems/debian/__init__.py new file mode 100644 index 00000000..a2b38d41 --- /dev/null +++ b/tests/gems/debian/__init__.py @@ -0,0 +1,3 @@ +import os + +DEBIAN_TESTS_DIR = os.path.dirname(os.path.abspath(__file__)) diff --git a/tests/gems/debian/resources/app_idx_full.json b/tests/gems/debian/resources/app_idx_full.json new file mode 100644 index 00000000..bd3d4989 --- /dev/null +++ b/tests/gems/debian/resources/app_idx_full.json @@ -0,0 +1,17 @@ +{ + "firefox": { + "categories": [ + "GNOME", + "GTK", + "Network", + "WebBrowser" + ], + "exe_path": "firefox %u", + "icon_path": "firefox" + }, + + "synaptic": { + "exe_path": "synaptic-pkexec", + "icon_path": "synaptic" + } +} diff --git a/tests/gems/debian/test_aptitude.py b/tests/gems/debian/test_aptitude.py new file mode 100644 index 00000000..eb16a73a --- /dev/null +++ b/tests/gems/debian/test_aptitude.py @@ -0,0 +1,169 @@ +from unittest import TestCase +from unittest.mock import Mock, patch + +from bauh import __app_name__ +from bauh.commons import system +from bauh.commons.system import USE_GLOBAL_INTERPRETER +from bauh.gems.debian.aptitude import Aptitude, map_package_name +from bauh.gems.debian.model import DebianPackage + + +class MapPackageNameTest(TestCase): + + def test__it_must_return_the_same_input_when_no_colon(self): + self.assertEqual('my_package', map_package_name('my_package')) + + def test__it_must_return_the_only_the_word_before_the_first_colon(self): + self.assertEqual('my_package', map_package_name('my_package:i386')) + + def test__it_must_return_a_name_with_colon_when_several_colons_are_present(self): + self.assertEqual('my_package:i386', map_package_name('my_package:i386:amd64')) + + +class AptitudeTest(TestCase): + + def setUp(self): + self.aptitude = Aptitude(Mock()) + + @patch(f'{__app_name__}.gems.debian.aptitude.system.execute', return_value=(0, """ +gimp-cbmplugs^^1.2.2-1build1^Distro Developers ^universe/graphics^plugins for The GIMP to import/export Commodore 64 files +gimp-gmic^2.4.5-1.0^2.4.5-1.1^Distro Developers ^plugin^GREYC's Magic for Image Computing - GIMP Plugin +gimp-gutenprint^5.3.3-4^5.3.3-4^Distro Developers ^plugin^print plugin for the GIMP +gimp-help^^^^^ + """)) + def test_search__must_return_installed_and_not_installed_packages_with_updates(self, execute: Mock): + query = 'gimp' + res = [p for p in self.aptitude.search(query=query)] + + execute.assert_called_once_with(f"aptitude search {query} -q -F '%p^%v^%V^%m^%s^%d' --disable-columns", + shell=True, custom_env=system.gen_env(USE_GLOBAL_INTERPRETER, lang='')) + + exp = [ + DebianPackage(name='gimp-cbmplugs', version='1.2.2-1build1', latest_version='1.2.2-1build1', + maintainer='Distro Developers', + description='plugins for The GIMP to import/export Commodore 64 files', + categories=('graphics',), + installed=False, update=False), + DebianPackage(name='gimp-gmic', version='2.4.5-1.0', latest_version='2.4.5-1.1', + maintainer='Distro Developers', + description="GREYC's Magic for Image Computing - GIMP Plugin", + categories=('plugin',), + installed=True, update=True), + DebianPackage(name='gimp-gutenprint', version='5.3.3-4', latest_version='5.3.3-4', + maintainer='Distro Developers', + description="print plugin for the GIMP", + categories=('plugin',), + installed=True, update=False), + ] + + self.assertEqual([p.__dict__ for p in exp], [p.__dict__ for p in res]) + + @patch(f'{__app_name__}.gems.debian.aptitude.system.execute', return_value=(0, """ +Package: firefox +Version: 97.0+distro1+una +State: installed (95.0.1+distro1.1+una), upgrade available (97.0+distro1+una) +Automatically installed: no +Priority: optional +Section: web +Maintainer: Distro Dev +Architecture: amd64 +Uncompressed Size: 236 M +PreDepends: distro-system-adjustments (>= 2021.12.16) +Breaks: firefox-dbg (< 95.0.1+distro1+una), firefox-dev (< 95.0.1+distro1+una), firefox-geckodriver (< 95.0.1+distro1+una), firefox-mozsymbols (< 95.0.1+distro1+una) +Replaces: firefox-dbg (< 95.0.1+distro1+una), firefox-dev (< 95.0.1+distro1+una), firefox-geckodriver (< 95.0.1+distro1+una), firefox-mozsymbols (< 95.0.1+distro1+una) +Provides: gnome-www-browser, www-browser +Description: The Firefox web browser + The Mozilla Firefox Web Browser. + +Package: gcc +Version: 4:9.3.0-1distro2 +State: installed +Automatically installed: no +Priority: optional +Section: devel +Maintainer: Distro Developers +Architecture: amd64 +Uncompressed Size: 51,2 k +Depends: cpp (= 4:9.3.0-1distro2), gcc-9 (>= 9.3.0-3~) +Recommends: libc6-dev | libc-dev +Suggests: gcc-multilib, make, manpages-dev, autoconf, automake, libtool, flex, bison, gdb, gcc-doc +Conflicts: gcc-doc (< 1:2.95.3), gcc-doc:i386 (< 1:2.95.3), gcc:i386 +Provides: c-compiler, gcc-x86-64-linux-gnu (= 4:9.3.0-1distro2), gcc:amd64 (= 4:9.3.0-1distro2) +Description: GNU C compiler + This is the GNU C compiler, a fairly portable optimizing compiler for C. + + This is a dependency package providing the default GNU C compiler. + +""")) + def test_show__all_attributes(self, execute: Mock): + info = self.aptitude.show(('firefox', 'gcc')) + execute.assert_called_once_with('aptitude show -q firefox gcc', shell=True, + custom_env=system.gen_env(global_interpreter=system.USE_GLOBAL_INTERPRETER, + lang='')) + + expected = { + 'firefox': { + 'version': '97.0+distro1+una', + 'state': ('installed (95.0.1+distro1.1+una)', 'upgrade available (97.0+distro1+una)'), + 'automatically installed': 'no', + 'priority': 'optional', + 'section': 'web', + 'maintainer': 'Distro Dev ', + 'architecture': 'amd64', + 'uncompressed size': 236000000, + 'predepends': ('distro-system-adjustments (>= 2021.12.16)', ), + 'breaks': ('firefox-dbg (< 95.0.1+distro1+una)', 'firefox-dev (< 95.0.1+distro1+una)', + 'firefox-geckodriver (< 95.0.1+distro1+una)', 'firefox-mozsymbols (< 95.0.1+distro1+una)'), + 'replaces': ('firefox-dbg (< 95.0.1+distro1+una)', 'firefox-dev (< 95.0.1+distro1+una)', + 'firefox-geckodriver (< 95.0.1+distro1+una)', 'firefox-mozsymbols (< 95.0.1+distro1+una)'), + 'provides': ('gnome-www-browser', 'www-browser'), + 'description': 'The Firefox web browser' + }, + 'gcc': { + 'version': '4:9.3.0-1distro2', + 'state': ('installed', ), + 'automatically installed': 'no', + 'priority': 'optional', + 'section': 'devel', + 'maintainer': 'Distro Developers ', + 'architecture': 'amd64', + 'uncompressed size': 51200, + 'depends': ('cpp (= 4:9.3.0-1distro2)', 'gcc-9 (>= 9.3.0-3~)'), + 'recommends': ('libc6-dev | libc-dev', ), + 'suggests': ('gcc-multilib', 'make', 'manpages-dev', 'autoconf', 'automake', + 'libtool', 'flex', 'bison', 'gdb', 'gcc-doc'), + 'conflicts': ('gcc-doc (< 1:2.95.3)', 'gcc-doc:i386 (< 1:2.95.3)', 'gcc:i386'), + 'provides': ('c-compiler', 'gcc-x86-64-linux-gnu (= 4:9.3.0-1distro2)', 'gcc:amd64 (= 4:9.3.0-1distro2)'), + 'description': 'GNU C compiler' + } + } + + self.assertEqual(expected, info) + + @patch(f'{__app_name__}.gems.debian.aptitude.system.execute', return_value=(0, """ + gir1.2-javascriptcoregtk-4.0^2.34.1-0distro0.20.04.1^2.34.4-0distro0.20.04.1^Distro Developers ^library^JavaScript engine library from WebKitGTK - GObject introspection data + gir1.2-nm-1.0^1.22.10-1distro2.2^1.22.10-1distro2.3^Distro Developers ^library^GObject introspection data for the libnm library + xwayland^2:1.20.13-1distro1~20.04.2^2:1.20.13-1distro1~20.04.2^Distro X-SWAT ^X11^Xwayland X server + """)) + def test_read_installed__with_updates_available(self, execute: Mock): + returned = [p for p in self.aptitude.read_installed()] + execute.assert_called_once() + + expected = [DebianPackage(name='gir1.2-javascriptcoregtk-4.0', version='2.34.1-0distro0.20.04.1', + latest_version='2.34.4-0distro0.20.04.1', + maintainer='Distro Developers', update=True, installed=True, + categories=('library',), + description='JavaScript engine library from WebKitGTK - GObject introspection data'), + DebianPackage(name='gir1.2-nm-1.0', version='1.22.10-1distro2.2', + latest_version='1.22.10-1distro2.3', + maintainer='Distro Developers', update=True, installed=True, + categories=('library',), + description='GObject introspection data for the libnm library'), + DebianPackage(name='xwayland', version='2:1.20.13-1distro1~20.04.2', + latest_version='2:1.20.13-1distro1~20.04.2', + maintainer='Distro X-SWAT', update=False, installed=True, + categories=('X11',), + description='Xwayland X server') + ] + + self.assertEqual([p.__dict__ for p in expected], [p.__dict__ for p in returned]) diff --git a/tests/gems/debian/test_controller.py b/tests/gems/debian/test_controller.py new file mode 100644 index 00000000..5a423a5a --- /dev/null +++ b/tests/gems/debian/test_controller.py @@ -0,0 +1,99 @@ +from unittest import TestCase +from unittest.mock import MagicMock, patch, Mock + +from bauh import __app_name__ +from bauh.api.abstract.controller import SearchResult +from bauh.gems.debian.controller import DebianPackageManager +from bauh.gems.debian.model import DebianPackage, DebianApplication + + +class DebianPackageManagerTest(TestCase): + + def setUp(self): + self.controller = DebianPackageManager(MagicMock()) + self.controller._apps_index = {} + + @patch(f'{__app_name__}.gems.debian.controller.Aptitude.read_installed', return_value=iter(( + DebianPackage(name='gir1.2-javascriptcoregtk-4.0', version='2.34.1-0distro0.20.04.1', + latest_version='2.34.1-0distro0.20.04.1', + maintainer='Distro Developers', update=False, installed=True, + description='JavaScript engine library from WebKitGTK - GObject introspection data'), + DebianPackage(name='xwayland', version='2:1.20.13-1distro1~20.04.2', + latest_version='2:1.20.13-1distro1~20.04.2', + maintainer='Distro X-SWAT', update=False, installed=True, + description='Xwayland X server') + ))) + def test_read_installed__must_associated_packages_found_to_applications_if_appliable(self, read_installed: Mock): + app = DebianApplication(name='xwayland', exe_path='xwayland', icon_path='xwayland', categories=('app',)) + + self.controller.__apps_index = {'xwayland': app} + + result = self.controller.read_installed(disk_loader=None, pkg_types=None, internet_available=False) + read_installed.assert_called_once() + + self.assertIsNone(result.new) + + expected = [DebianPackage(name='gir1.2-javascriptcoregtk-4.0', version='2.34.1-0distro0.20.04.1', latest_version='2.34.1-0distro0.20.04.1', + maintainer='Distro Developers', update=False, installed=True, + description='JavaScript engine library from WebKitGTK - GObject introspection data'), + DebianPackage(name='xwayland', version='2:1.20.13-1distro1~20.04.2', latest_version='2:1.20.13-1distro1~20.04.2', + maintainer='Distro X-SWAT', update=False, installed=True, + description='Xwayland X server', app=app) + ] + + self.assertEqual(expected, result.installed) + + @patch(f'{__app_name__}.gems.debian.controller.Aptitude.read_installed', return_value=iter(( + DebianPackage(name='gir1.2-javascriptcoregtk-4.0', version='2.34.1-0distro0.20.04.1', + latest_version='2.34.1-0distro0.20.04.1', + maintainer='Distro Developers', update=False, installed=True, + description='JavaScript engine library from WebKitGTK - GObject introspection data'), + DebianPackage(name='xwayland', version='2:1.20.13-1distro1~20.04.2', + latest_version='2:1.20.13-1distro1~20.04.2', + maintainer='Distro X-SWAT', update=False, installed=True, + description='Xwayland X server') + ))) + def test_read_installed__internet_not_available(self, read_installed: Mock): + result = self.controller.read_installed(disk_loader=None, pkg_types=None, internet_available=False) + read_installed.assert_called_once() + + self.assertIsNone(result.new) + + expected = [DebianPackage(name='gir1.2-javascriptcoregtk-4.0', version='2.34.1-0distro0.20.04.1', latest_version='2.34.1-0distro0.20.04.1', + maintainer='Distro Developers', update=False, installed=True, + description='JavaScript engine library from WebKitGTK - GObject introspection data'), + DebianPackage(name='xwayland', version='2:1.20.13-1distro1~20.04.2', latest_version='2:1.20.13-1distro1~20.04.2', + maintainer='Distro X-SWAT', update=False, installed=True, + description='Xwayland X server') + ] + + self.assertEqual(expected, result.installed) + + @patch(f'{__app_name__}.gems.debian.controller.Aptitude.read_installed') + def test_search__must_return_empty_result_when_url(self, read_installed: Mock): + words = 'i' + res = self.controller.search(words=words, disk_loader=None, limit=-1, is_url=True) + read_installed.assert_not_called() + self.assertEqual(SearchResult.empty(), res) + + @patch(f'{__app_name__}.gems.debian.controller.Aptitude.search', return_value=iter(( + DebianPackage(name='xpto', version='1.0', latest_version='1.0', installed=True, update=False, description=''), + DebianPackage(name='test', version='1.0', latest_version='1.0', installed=False, update=False, description=''), + DebianPackage(name='myapp', version='1.0', latest_version='1.0', installed=True, update=False, description=''), + ))) + def test_search__returned_packages_should_be_associated_with_apps_if_appliable(self, search: Mock): + app = DebianApplication(name='myapp', exe_path='myapp', icon_path='myapp', + categories=('app',)) + self.controller._apps_index = {'myapp': app} + + words = 'test' + res = self.controller.search(words=words, disk_loader=None, limit=-1, is_url=False) + search.assert_called_once_with(words) + + self.assertEqual([DebianPackage(name='xpto', version='1.0', latest_version='1.0', installed=True, update=False, description=''), + DebianPackage(name='myapp', version='1.0', latest_version='1.0', installed=True, update=False, + description='', app=app) + ], res.installed) + + self.assertEqual([DebianPackage(name='test', version='1.0', latest_version='1.0', installed=False, update=False, description='')], + res.new) diff --git a/tests/gems/debian/test_index.py b/tests/gems/debian/test_index.py new file mode 100644 index 00000000..01a5de24 --- /dev/null +++ b/tests/gems/debian/test_index.py @@ -0,0 +1,171 @@ +import json +import os.path +from unittest import TestCase +from unittest.mock import Mock, patch, call + +from bauh import __app_name__ +from bauh.gems.debian.index import ApplicationsMapper, ApplicationIndexer +from bauh.gems.debian.model import DebianApplication +from tests.gems.debian import DEBIAN_TESTS_DIR + +def mock_read_file(fpath: str): + if fpath.endswith('firefox.desktop'): + return """ + [Desktop Entry] + Version=1.0 + Name=Firefox Web Browser + Comment=Browse the World Wide Web + GenericName=Web Browser + Keywords=Internet;WWW;Browser;Web;Explorer + Exec=firefox %u + Terminal=false + X-MultipleArgs=false + Type=Application + Icon=firefox + Categories=GNOME;GTK;Network;WebBrowser; + """ + elif fpath.endswith('synaptic.desktop'): + return """ + [Desktop Entry] + Name=Synaptic Package Manager + GenericName=Package Manager + Comment=Install, remove and upgrade software packages + Exec=synaptic-pkexec + Icon=synaptic + Terminal=false + Type=Application + """ + elif fpath.endswith('no-icon.desktop'): + return """ + [Desktop Entry] + Name=No-icon + Exec=no-icon + Terminal=false + Type=Application + Categories=PackageManager;GTK;System;Settings; + """ + elif fpath.endswith('no-exe.desktop'): + return """ + [Desktop Entry] + Name=No-exe + Terminal=false + Type=Application + Icon=no-exe + Categories=PackageManager;GTK;System;Settings; + """ + elif fpath.endswith('no-display.desktop'): + return """ + [Desktop Entry] + Name=No-display + Exec=no-display + Terminal=false + Type=Application + Icon=no-display + NoDisplay=true + Categories=PackageManager;GTK;System;Settings; + """ + elif fpath.endswith('terminal-app.desktop'): + return """ + [Desktop Entry] + Name=Terminal-App + Exec=terminal-app + Terminal=true + Type=Application + Icon=terminal-app + Categories=PackageManager;GTK;System;Settings; + """ + + +class ApplicationsMapperTest(TestCase): + + def setUp(self): + self.mapper = ApplicationsMapper(logger=Mock(), workers=1) + + @patch(f'{__app_name__}.gems.debian.index.ApplicationsMapper._read_file', side_effect=mock_read_file) + @patch(f'{__app_name__}.gems.debian.index.system.execute', return_value=(0, """ + firefox: /usr/share/applications/firefox.desktop + app-install-data: /usr/share/app-install/desktop/firefox-launchpad-plugin.desktop + app-install-data: /usr/share/app-install/desktop/firefox:firefox.desktop + xfce4-helpers: /usr/share/xfce4/helpers/firefox.desktop + synaptic: /usr/share/applications/synaptic.desktop + no-icon: /usr/share/applications/no-icon.desktop + no-exe: /usr/share/applications/no-exe.desktop + no-display: /usr/share/applications/no-display.desktop + terminal-app: /usr/share/applications/terminal-app.desktop + """)) + def test_map_executable_applications__return_applications_with_exec_and_icon(self, execute: Mock, read_file: Mock): + apps = self.mapper.map_executable_applications() + execute.assert_called_once_with('dpkg-query -S .desktop', shell=True) + read_file.assert_has_calls([call('/usr/share/applications/firefox.desktop'), + call('/usr/share/applications/synaptic.desktop'), + call('/usr/share/applications/no-icon.desktop'), + call('/usr/share/applications/no-exe.desktop')], any_order=True) + + self.assertEqual({ + DebianApplication(name='firefox', exe_path='firefox %u', icon_path='firefox', + categories=('GNOME', 'GTK', 'Network', 'WebBrowser')), + DebianApplication(name='synaptic', exe_path='synaptic-pkexec', icon_path='synaptic', + categories=None) + }, apps) + + +class ApplicationIndexerTest(TestCase): + + def setUp(self): + self.update_idx_file_path = f'{DEBIAN_TESTS_DIR}/resources/apps_idx.json' + self.update_idx_ts_file_path = f'{self.update_idx_file_path}.ts' + self.app_indexer = ApplicationIndexer(logger=Mock(), + index_file_path=self.update_idx_file_path) + + if os.path.exists(self.update_idx_file_path): + os.remove(self.update_idx_file_path) + + def tearDown(self) -> None: + if os.path.exists(self.update_idx_file_path): + os.remove(self.update_idx_file_path) + + if os.path.exists(self.update_idx_ts_file_path): + os.remove(self.update_idx_ts_file_path) + + def test_update_index(self): + apps = { + DebianApplication(name='firefox', exe_path='firefox %u', icon_path='firefox', + categories=('GNOME', 'GTK', 'Network', 'WebBrowser')), + DebianApplication(name='synaptic', exe_path='synaptic-pkexec', icon_path='synaptic', + categories=None) + } + + self.app_indexer.update_index(apps) + + self.assertTrue(os.path.exists(self.update_idx_file_path)) + + with open(self.update_idx_file_path) as f: + index_content = f.read() + + self.assertEqual({'firefox': {'exe_path': 'firefox %u', 'icon_path': 'firefox', + 'categories': ['GNOME', 'GTK', 'Network', 'WebBrowser']}, + 'synaptic': {'exe_path': 'synaptic-pkexec', 'icon_path': 'synaptic', + 'categories': None} + }, json.loads(index_content)) + + self.assertTrue(os.path.isfile(self.update_idx_ts_file_path)) + + with open(self.update_idx_ts_file_path) as f: + ts_str = f.read() + + try: + float(ts_str) + except ValueError: + self.assertFalse(False, "index timestamp must be a float number") + + def test_read_index(self): + self.app_indexer._file_path = f'{DEBIAN_TESTS_DIR}/resources/app_idx_full.json' + + expected = { + DebianApplication(name='firefox', exe_path='firefox %u', icon_path='firefox', + categories=('GNOME', 'GTK', 'Network', 'WebBrowser')), + DebianApplication(name='synaptic', exe_path='synaptic-pkexec', icon_path='synaptic', + categories=None) + } + + self.assertEqual(expected, {app for app in self.app_indexer.read_index()})