mirror of
https://github.com/spalencsar/bearhub.git
synced 2026-07-07 00:04:15 +02:00
gem selector sketch
This commit is contained in:
0
bauh/api/__init__.py
Normal file
0
bauh/api/__init__.py
Normal file
0
bauh/api/abstract/__init__.py
Normal file
0
bauh/api/abstract/__init__.py
Normal file
50
bauh/api/abstract/cache.py
Normal file
50
bauh/api/abstract/cache.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Set
|
||||||
|
|
||||||
|
|
||||||
|
class MemoryCache(ABC):
|
||||||
|
"""
|
||||||
|
Represents a memory cache.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def is_enabled(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def add(self, key: str, val: object):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def add_non_existing(self, key: str, val: object):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get(self, key: str):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def delete(self, key):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def keys(self) -> Set[str]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def clean_expired(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MemoryCacheFactory(ABC):
|
||||||
|
"""
|
||||||
|
Instantiate new memory cache instances.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def new(self, expiration: int) -> MemoryCache:
|
||||||
|
"""
|
||||||
|
:param expiration: expiration time for the cache keys in seconds. Use -1 to disable this feature.
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
pass
|
||||||
30
bauh/api/abstract/context.py
Normal file
30
bauh/api/abstract/context.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from bauh.api.abstract.cache import MemoryCacheFactory
|
||||||
|
from bauh.api.abstract.disk import DiskCacheLoaderFactory
|
||||||
|
from bauh.api.http import HttpClient
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationContext:
|
||||||
|
|
||||||
|
def __init__(self, disk_cache: bool, download_icons: bool, http_client: HttpClient, app_root_dir: str, i18n: dict,
|
||||||
|
cache_factory: MemoryCacheFactory, disk_loader_factory: DiskCacheLoaderFactory,
|
||||||
|
logger: logging.Logger):
|
||||||
|
"""
|
||||||
|
:param disk_cache: if package data should be cached to disk
|
||||||
|
:param download_icons: if packages icons should be downloaded
|
||||||
|
:param http_client: a shared instance of http client
|
||||||
|
:param app_root_dir: GUI root dir
|
||||||
|
:param i18n: the i18n dictionary keys
|
||||||
|
:param cache_factory:
|
||||||
|
:param disk_loader_factory:
|
||||||
|
:param logger: a logger instance
|
||||||
|
"""
|
||||||
|
self.disk_cache = disk_cache
|
||||||
|
self.download_icons = download_icons
|
||||||
|
self.http_client = http_client
|
||||||
|
self.app_root_dir = app_root_dir
|
||||||
|
self.i18n = i18n
|
||||||
|
self.cache_factory = cache_factory
|
||||||
|
self.disk_loader_factory = disk_loader_factory
|
||||||
|
self.logger = logger
|
||||||
214
bauh/api/abstract/controller.py
Normal file
214
bauh/api/abstract/controller.py
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Set, Type
|
||||||
|
|
||||||
|
from bauh.api.abstract.context import ApplicationContext
|
||||||
|
from bauh.api.abstract.disk import DiskCacheLoader
|
||||||
|
from bauh.api.abstract.handler import ProcessWatcher
|
||||||
|
from bauh.api.abstract.model import SoftwarePackage, PackageUpdate, PackageHistory, PackageSuggestion, PackageAction
|
||||||
|
|
||||||
|
|
||||||
|
class SearchResult:
|
||||||
|
|
||||||
|
def __init__(self, installed: List[SoftwarePackage], new: List[SoftwarePackage], total: int):
|
||||||
|
"""
|
||||||
|
:param installed: already installed packages
|
||||||
|
:param new: new packages found
|
||||||
|
:param total: total number of applications actually found
|
||||||
|
"""
|
||||||
|
self.installed = installed
|
||||||
|
self.new = new
|
||||||
|
self.total = total
|
||||||
|
|
||||||
|
|
||||||
|
class SoftwareManager(ABC):
|
||||||
|
|
||||||
|
"""
|
||||||
|
Base controller class that will be called by the graphical interface to execute operations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, context: ApplicationContext):
|
||||||
|
"""
|
||||||
|
:param context:
|
||||||
|
"""
|
||||||
|
self.context = context
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def search(self, words: str, disk_loader: DiskCacheLoader, limit: int) -> SearchResult:
|
||||||
|
"""
|
||||||
|
:param words: the words typed by the user
|
||||||
|
:param disk_loader: a running disk loader thread that loads package data from the disk asynchronously
|
||||||
|
:param limit: the max number of packages to be retrieved. <= 1 should retrieve everything
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def read_installed(self, disk_loader: DiskCacheLoader, limit: int, only_apps: bool, pkg_types: Set[Type[SoftwarePackage]]) -> SearchResult:
|
||||||
|
"""
|
||||||
|
:param disk_loader: a running disk loader thread that loads application data from the disk asynchronously
|
||||||
|
:param limit: the max number of packages to be retrieved. <= 1 should retrieve everything
|
||||||
|
:param only_apps: if only application packages should be retrieved
|
||||||
|
:param pkg_types: use 'None' to bring any or specify some
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def downgrade(self, pkg: SoftwarePackage, root_password: str, handler: ProcessWatcher) -> bool:
|
||||||
|
"""
|
||||||
|
downgrades a package version
|
||||||
|
:param pkg:
|
||||||
|
:param root_password: the root user password (if required)
|
||||||
|
:param handler: a subprocess handler
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def clean_cache_for(self, pkg: SoftwarePackage):
|
||||||
|
"""
|
||||||
|
Cleans cached package cached data. This default implementation only cleans the cached data from the heard disk
|
||||||
|
:param pkg:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if pkg.supports_disk_cache() and os.path.exists(pkg.get_disk_cache_path()):
|
||||||
|
shutil.rmtree(pkg.get_disk_cache_path())
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def update(self, pkg: SoftwarePackage, root_password: str, watcher: ProcessWatcher) -> bool:
|
||||||
|
"""
|
||||||
|
:param pkg:
|
||||||
|
:param root_password: the root user password (if required)
|
||||||
|
:param watcher:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def uninstall(self, pkg: SoftwarePackage, root_password: str, watcher: ProcessWatcher) -> bool:
|
||||||
|
"""
|
||||||
|
:param pkg:
|
||||||
|
:param root_password: the root user password (if required)
|
||||||
|
:param watcher:
|
||||||
|
:return: if the uninstall succeeded
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_managed_types(self) -> Set[Type[SoftwarePackage]]:
|
||||||
|
"""
|
||||||
|
:return: the managed package class type
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_info(self, pkg: SoftwarePackage) -> dict:
|
||||||
|
"""
|
||||||
|
retrieve the package information
|
||||||
|
:param pkg:
|
||||||
|
:return: a dictionary with the attributes to be shown
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_history(self, pkg: SoftwarePackage) -> PackageHistory:
|
||||||
|
"""
|
||||||
|
:param pkg:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def install(self, pkg: SoftwarePackage, root_password: str, watcher: ProcessWatcher) -> bool:
|
||||||
|
"""
|
||||||
|
:param pkg:
|
||||||
|
:param root_password: the root user password (if required)
|
||||||
|
:param watcher:
|
||||||
|
:return: if the installation succeeded
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def is_enabled(self) -> bool:
|
||||||
|
"""
|
||||||
|
:return: if the instance is available to perform actions
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def cache_to_disk(self, pkg: SoftwarePackage, icon_bytes: bytes, only_icon: bool):
|
||||||
|
"""
|
||||||
|
Saves the package data to the hard disk.
|
||||||
|
:param pkg:
|
||||||
|
:param icon_bytes:
|
||||||
|
:param only_icon: if only the icon should be saved
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if pkg.supports_disk_cache():
|
||||||
|
|
||||||
|
if not only_icon:
|
||||||
|
Path(pkg.get_disk_cache_path()).mkdir(parents=True, exist_ok=True)
|
||||||
|
data = pkg.get_data_to_cache()
|
||||||
|
|
||||||
|
if data:
|
||||||
|
with open(pkg.get_disk_data_path(), 'w+') as f:
|
||||||
|
f.write(json.dumps(data))
|
||||||
|
|
||||||
|
if icon_bytes:
|
||||||
|
Path(pkg.get_disk_cache_path()).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
with open(pkg.get_disk_icon_path(), 'wb+') as f:
|
||||||
|
f.write(icon_bytes)
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def requires_root(self, action: str, pkg: SoftwarePackage):
|
||||||
|
"""
|
||||||
|
if a given action requires root privileges to be executed. Current actions are: 'install', 'uninstall', 'downgrade', 'search', 'refresh'
|
||||||
|
:param action:
|
||||||
|
:param pkg:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def prepare(self):
|
||||||
|
"""
|
||||||
|
It prepares the manager to start working. It will be called by GUI. Do not call it within.
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def list_updates(self) -> List[PackageUpdate]:
|
||||||
|
"""
|
||||||
|
:return: available package updates
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def list_warnings(self) -> List[str]:
|
||||||
|
"""
|
||||||
|
:return: a list of warnings to be shown to the user
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def list_suggestions(self, limit: int) -> List[PackageSuggestion]:
|
||||||
|
"""
|
||||||
|
:param limit: max suggestions to be returned. If limit < 0, it should not be considered
|
||||||
|
:return: a list of package suggestions
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def execute_custom_action(self, action: PackageAction, pkg: SoftwarePackage, root_password: str, watcher: ProcessWatcher) -> bool:
|
||||||
|
"""
|
||||||
|
At the moment the GUI implements this action. No need to implement it yourself.
|
||||||
|
:param action:
|
||||||
|
:param pkg:
|
||||||
|
:param root_password:
|
||||||
|
:param watcher:
|
||||||
|
:return: if the action resulted in success
|
||||||
|
"""
|
||||||
|
pass
|
||||||
42
bauh/api/abstract/disk.py
Normal file
42
bauh/api/abstract/disk.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
|
from bauh.api.abstract.cache import MemoryCache
|
||||||
|
from bauh.api.abstract.model import SoftwarePackage
|
||||||
|
|
||||||
|
|
||||||
|
class DiskCacheLoader:
|
||||||
|
"""
|
||||||
|
Reads cached data from the disk and fills package instances.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def map(self, cache: MemoryCache, pkg_type: Type[SoftwarePackage]):
|
||||||
|
"""
|
||||||
|
maps a given cache instance for a given package type
|
||||||
|
:param cache:
|
||||||
|
:param pkg_type:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def fill(self, pkg: SoftwarePackage):
|
||||||
|
"""
|
||||||
|
fill cached data from the disk of a given package instance
|
||||||
|
If a cache mapping was previously done, then data retrieved will be cached to memory as well.
|
||||||
|
:param pkg:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DiskCacheLoaderFactory(ABC):
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def map(self, pkg_type: Type[SoftwarePackage], cache: MemoryCache):
|
||||||
|
"""
|
||||||
|
Associated a cache instance to instances of a given SoftwarePackage class
|
||||||
|
:param pkg_type:
|
||||||
|
:param cache:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
pass
|
||||||
67
bauh/api/abstract/handler.py
Normal file
67
bauh/api/abstract/handler.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
from typing import List
|
||||||
|
|
||||||
|
from bauh.api.abstract.view import MessageType, ViewComponent
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessWatcher:
|
||||||
|
"""
|
||||||
|
Represents an view component watching background processes. It's a bridge for ApplicationManager instances notifiy the view of processes progression and also
|
||||||
|
request any user interaction without the need of knowing any GUI code.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def print(self, msg: str):
|
||||||
|
"""
|
||||||
|
prints a given message to the user. In the current GUI implementation, the message is printed on the terminal window ("Details")
|
||||||
|
:param msg:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def request_confirmation(self, title: str, body: str, components: List[ViewComponent] = None, confirmation_label: str = None, deny_label: str = None)-> bool:
|
||||||
|
"""
|
||||||
|
request a user confirmation. In the current GUI implementation, it shows a popup to the user.
|
||||||
|
:param title: popup title
|
||||||
|
:param body: popup body
|
||||||
|
:param components: extra view components that will be rendered to the confirmation popup.
|
||||||
|
:param confirmation_label: optional confirmation button label (default to 'yes')
|
||||||
|
:param deny_label: optional deny button label (default to 'no')
|
||||||
|
:return: if the request was confirmed by the user
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def show_message(self, title: str, body: str, type_: MessageType = MessageType.INFO):
|
||||||
|
"""
|
||||||
|
shows a message to the user. In the current GUI implementation, it shows a popup.
|
||||||
|
:param title:
|
||||||
|
:param body:
|
||||||
|
:param type_: determines the icon that will be displayed
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def change_status(self, msg: str):
|
||||||
|
"""
|
||||||
|
Changes the process status. In the current GUI implementation, the process status is displayed above the toolbar.
|
||||||
|
:param msg: msg
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def change_substatus(self, msg: str):
|
||||||
|
"""
|
||||||
|
Changes the process substatus. In the current GUI implementation, the process substatus is displayed above the progress bar.
|
||||||
|
:param msg:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
def change_progress(self, val: int):
|
||||||
|
"""
|
||||||
|
Changes the process progress. In the current GUI implementation, the progress is displayed as a bottom bar.
|
||||||
|
:param val: a val between 0 and 100
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
def should_stop(self) -> bool:
|
||||||
|
"""
|
||||||
|
:return: if the use requested to stop the process.
|
||||||
|
"""
|
||||||
222
bauh/api/abstract/model.py
Normal file
222
bauh/api/abstract/model.py
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from enum import Enum
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from bauh.api.constants import CACHE_PATH
|
||||||
|
|
||||||
|
|
||||||
|
class PackageAction:
|
||||||
|
|
||||||
|
def __init__(self, i18_label_key: str, i18n_status_key: str, icon_path: str, manager_method: str, requires_root: bool):
|
||||||
|
"""
|
||||||
|
:param i18_label_key: the i18n key that will be used to display the action name
|
||||||
|
:param i18n_status_key: the i18n key that will be used to display the action name being executed
|
||||||
|
:param icon_path: the action icon path. Use None for no icon
|
||||||
|
:param manager_method: the SoftwareManager method name that should be called. The method must has the following parameters: (pkg: SoftwarePackage, root_password: str, watcher: ProcessWatcher)
|
||||||
|
:param requires_root:
|
||||||
|
"""
|
||||||
|
self.id = id
|
||||||
|
self.i18_label_key = i18_label_key
|
||||||
|
self.i18n_status_key = i18n_status_key
|
||||||
|
self.icon_path = icon_path
|
||||||
|
self.manager_method = manager_method
|
||||||
|
self.requires_root = requires_root
|
||||||
|
|
||||||
|
|
||||||
|
class PackageStatus(Enum):
|
||||||
|
READY = 1 # when all package data is already filled
|
||||||
|
LOADING_DATA = 2 # when some package data is already being retrieved asynchronously
|
||||||
|
|
||||||
|
|
||||||
|
class SoftwarePackage(ABC):
|
||||||
|
|
||||||
|
def __init__(self, id: str = None, version: str = None, name: str = None, description: str = None, latest_version: str = None,
|
||||||
|
icon_url: str = None, status: PackageStatus = PackageStatus.READY, installed: bool = False, update: bool = False,
|
||||||
|
size: int = None, categories: List[str] = None):
|
||||||
|
"""
|
||||||
|
:param id:
|
||||||
|
:param version:
|
||||||
|
:param name:
|
||||||
|
:param description:
|
||||||
|
:param latest_version:
|
||||||
|
:param icon_url: the path to the package icon. It can be an URL or PATH
|
||||||
|
:param status:
|
||||||
|
:param installed:
|
||||||
|
:param update: if there is an update for package
|
||||||
|
:param size: package size in BYTES
|
||||||
|
:param categories: package categories. i.e: video editor, web browser, etc
|
||||||
|
"""
|
||||||
|
self.id = id
|
||||||
|
self.name = name
|
||||||
|
self.description = description
|
||||||
|
self.status = status
|
||||||
|
self.version = version
|
||||||
|
self.latest_version = latest_version
|
||||||
|
self.icon_url = icon_url
|
||||||
|
self.installed = installed
|
||||||
|
self.update = update
|
||||||
|
self.size = size
|
||||||
|
self.categories = categories
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def has_history(self):
|
||||||
|
"""
|
||||||
|
:return: if the application has a commit history that can be shown to the user
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def has_info(self):
|
||||||
|
"""
|
||||||
|
:return: if the application has additional information that can be shown to the user
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def can_be_downgraded(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def can_be_uninstalled(self):
|
||||||
|
return self.installed
|
||||||
|
|
||||||
|
def can_be_installed(self):
|
||||||
|
return not self.installed
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_type(self):
|
||||||
|
"""
|
||||||
|
:return: a string that represents the application type
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_default_icon_path(self):
|
||||||
|
"""
|
||||||
|
:return: the path of a default icon when the application icon could not be retrieved (or will not be retrieved)
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_type_icon_path(self):
|
||||||
|
"""
|
||||||
|
:return: the path of the application type icon
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def is_application(self):
|
||||||
|
"""
|
||||||
|
:return: if the package is an application
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def supports_disk_cache(self):
|
||||||
|
"""
|
||||||
|
:return: if some application data and icon should be cached to the user disk
|
||||||
|
"""
|
||||||
|
return self.installed and self.is_application()
|
||||||
|
|
||||||
|
def get_disk_cache_path(self):
|
||||||
|
"""
|
||||||
|
:return: base cache path for the specific app type
|
||||||
|
"""
|
||||||
|
return CACHE_PATH + '/' + self.get_type()
|
||||||
|
|
||||||
|
def get_disk_icon_path(self):
|
||||||
|
return '{}/icon.png'.format(self.get_disk_cache_path())
|
||||||
|
|
||||||
|
def get_disk_data_path(self):
|
||||||
|
return '{}/data.json'.format(self.get_disk_cache_path())
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_data_to_cache(self) -> dict:
|
||||||
|
"""
|
||||||
|
:return: the application data that should be cached in disk / memory for quick access
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def fill_cached_data(self, data: dict):
|
||||||
|
"""
|
||||||
|
sets cached data to the current instance
|
||||||
|
:param data:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def can_be_run(self) -> str:
|
||||||
|
"""
|
||||||
|
:return: whether the app can be run via the GUI
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_command(self) -> str:
|
||||||
|
"""
|
||||||
|
:return: the command to run the application
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def is_trustable(self) -> bool:
|
||||||
|
"""
|
||||||
|
:return: if the package is distributed by a trustable source
|
||||||
|
"""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_publisher(self) -> str:
|
||||||
|
"""
|
||||||
|
:return: the package publisher / maintainer
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_custom_supported_actions(self) -> List[PackageAction]:
|
||||||
|
"""
|
||||||
|
:return: custom supported actions
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '{} (id={}, name={})'.format(self.__class__.__name__, self.id, self.name)
|
||||||
|
|
||||||
|
|
||||||
|
class PackageUpdate:
|
||||||
|
|
||||||
|
def __init__(self, pkg_id: str, version: str, pkg_type: str):
|
||||||
|
"""
|
||||||
|
:param pkg_id: an unique package identifier
|
||||||
|
:param version: the new version
|
||||||
|
:param pkg_type: the package type
|
||||||
|
"""
|
||||||
|
self.id = pkg_id
|
||||||
|
self.version = version
|
||||||
|
self.type = pkg_type
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '{} (id={}, type={}, new_version={})'.format(self.__class__.__name__, self.id, self.type, self.type)
|
||||||
|
|
||||||
|
|
||||||
|
class PackageHistory:
|
||||||
|
|
||||||
|
def __init__(self, pkg: SoftwarePackage, history: List[dict], pkg_status_idx: int):
|
||||||
|
"""
|
||||||
|
:param pkg
|
||||||
|
:param history: a list with the package history.
|
||||||
|
:param pkg_status_idx: 'history' index in which the application is current found
|
||||||
|
"""
|
||||||
|
self.pkg = pkg
|
||||||
|
self.history = history
|
||||||
|
self.pkg_status_idx = pkg_status_idx
|
||||||
|
|
||||||
|
|
||||||
|
class SuggestionPriority(Enum):
|
||||||
|
LOW = 0
|
||||||
|
MEDIUM = 1
|
||||||
|
HIGH = 2
|
||||||
|
TOP = 3
|
||||||
|
|
||||||
|
|
||||||
|
class PackageSuggestion:
|
||||||
|
|
||||||
|
def __init__(self, package: SoftwarePackage, priority: SuggestionPriority):
|
||||||
|
self.package = package
|
||||||
|
self.priority = priority
|
||||||
90
bauh/api/abstract/view.py
Normal file
90
bauh/api/abstract/view.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
from abc import ABC
|
||||||
|
from enum import Enum
|
||||||
|
from typing import List, Set
|
||||||
|
|
||||||
|
|
||||||
|
class MessageType:
|
||||||
|
INFO = 0
|
||||||
|
WARNING = 1
|
||||||
|
ERROR = 2
|
||||||
|
|
||||||
|
|
||||||
|
class ViewComponent(ABC):
|
||||||
|
"""
|
||||||
|
Represents a GUI component
|
||||||
|
"""
|
||||||
|
def __init__(self, id_: str):
|
||||||
|
self.id = id_
|
||||||
|
|
||||||
|
|
||||||
|
class InputViewComponent(ViewComponent):
|
||||||
|
"""
|
||||||
|
Represents an component which needs a user interaction to provide its value
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class InputOption:
|
||||||
|
"""
|
||||||
|
Represents a select component option.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, label: str, value: str, tooltip: str = None, icon_path: str = None):
|
||||||
|
"""
|
||||||
|
:param label: the string that will be shown to the user
|
||||||
|
:param value: the option value (not shown)
|
||||||
|
:param tooltip: an optional tooltip
|
||||||
|
:param icon_path: an optional icon path
|
||||||
|
"""
|
||||||
|
if not label:
|
||||||
|
raise Exception("'label' must be a not blank string")
|
||||||
|
|
||||||
|
if not value:
|
||||||
|
raise Exception("'value' must be a not blank string")
|
||||||
|
|
||||||
|
self.label = label
|
||||||
|
self.value = value
|
||||||
|
self.tooltip = tooltip
|
||||||
|
self.icon_path = icon_path
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(self.label) + hash(self.value)
|
||||||
|
|
||||||
|
|
||||||
|
class SelectViewType(Enum):
|
||||||
|
RADIO = 0
|
||||||
|
COMBO = 1
|
||||||
|
|
||||||
|
|
||||||
|
class SingleSelectComponent(InputViewComponent):
|
||||||
|
|
||||||
|
def __init__(self, type_: SelectViewType, label: str, options: List[InputOption], default_option: InputOption = None, max_per_line: int = 1, id_: str = None):
|
||||||
|
super(SingleSelectComponent, self).__init__(id_=id_)
|
||||||
|
if options is None or len(options) < 2:
|
||||||
|
raise Exception("'options' must be a list with at least 2 elements")
|
||||||
|
|
||||||
|
self.type = type_
|
||||||
|
self.label = label
|
||||||
|
self.options = options
|
||||||
|
self.value = default_option
|
||||||
|
self.max_per_line = max_per_line
|
||||||
|
|
||||||
|
|
||||||
|
class MultipleSelectComponent(InputViewComponent):
|
||||||
|
|
||||||
|
def __init__(self, label: str, options: List[InputOption], default_options: Set[InputOption] = None, max_per_line: int = 1, id_: str = None):
|
||||||
|
super(MultipleSelectComponent, self).__init__(id_=id_)
|
||||||
|
|
||||||
|
if not options:
|
||||||
|
raise Exception("'options' cannot be None or empty")
|
||||||
|
|
||||||
|
self.options = options
|
||||||
|
self.label = label
|
||||||
|
self.values = default_options if default_options else set()
|
||||||
|
self.max_per_line = max_per_line
|
||||||
|
|
||||||
|
|
||||||
|
class TextComponent(ViewComponent):
|
||||||
|
|
||||||
|
def __init__(self, html: str, id_: str = None):
|
||||||
|
super(TextComponent, self).__init__(id_=id_)
|
||||||
|
self.value = html
|
||||||
4
bauh/api/constants.py
Normal file
4
bauh/api/constants.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
HOME_PATH = Path.home()
|
||||||
|
CACHE_PATH = '{}/.cache/bauh'.format(HOME_PATH)
|
||||||
3
bauh/api/exception.py
Normal file
3
bauh/api/exception.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
class NoInternetException(Exception):
|
||||||
|
pass
|
||||||
40
bauh/api/http.py
Normal file
40
bauh/api/http.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
class HttpClient:
|
||||||
|
|
||||||
|
def __init__(self, logger: logging.Logger, max_attempts: int = 2, timeout: int = 30, sleep: float = 0.5):
|
||||||
|
self.max_attempts = max_attempts
|
||||||
|
self.session = requests.Session()
|
||||||
|
self.timeout = timeout
|
||||||
|
self.sleep = sleep
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
|
def get(self, url: str):
|
||||||
|
cur_attempts = 1
|
||||||
|
|
||||||
|
while cur_attempts <= self.max_attempts:
|
||||||
|
cur_attempts += 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
res = self.session.get(url, timeout=self.timeout)
|
||||||
|
|
||||||
|
if res.status_code == 200:
|
||||||
|
return res
|
||||||
|
|
||||||
|
if self.sleep > 0:
|
||||||
|
time.sleep(self.sleep)
|
||||||
|
except:
|
||||||
|
self.logger.error("Could not retrieve data from '{}'".format(url))
|
||||||
|
traceback.print_exc()
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.logger.warning("Could not retrieve data from '{}'".format(url))
|
||||||
|
|
||||||
|
def get_json(self, url: str):
|
||||||
|
res = self.get(url)
|
||||||
|
return res.json() if res else None
|
||||||
68
bauh/app.py
68
bauh/app.py
@@ -2,15 +2,16 @@ import sys
|
|||||||
|
|
||||||
from PyQt5.QtGui import QIcon
|
from PyQt5.QtGui import QIcon
|
||||||
from PyQt5.QtWidgets import QApplication
|
from PyQt5.QtWidgets import QApplication
|
||||||
from bauh_api.abstract.controller import ApplicationContext
|
|
||||||
from bauh_api.http import HttpClient
|
|
||||||
|
|
||||||
from bauh import __version__, __app_name__, app_args, ROOT_DIR
|
from bauh import __version__, __app_name__, app_args, ROOT_DIR
|
||||||
from bauh.core import extensions
|
from bauh.api.abstract.controller import ApplicationContext
|
||||||
|
from bauh.api.http import HttpClient
|
||||||
|
from bauh.core import gems, config
|
||||||
from bauh.core.controller import GenericSoftwareManager
|
from bauh.core.controller import GenericSoftwareManager
|
||||||
from bauh.util import util, logs, resource
|
from bauh.util import util, logs, resource
|
||||||
from bauh.util.cache import DefaultMemoryCacheFactory, CacheCleaner
|
from bauh.util.cache import DefaultMemoryCacheFactory, CacheCleaner
|
||||||
from bauh.util.disk import DefaultDiskCacheLoaderFactory
|
from bauh.util.disk import DefaultDiskCacheLoaderFactory
|
||||||
|
from bauh.view.qt.gem_selector import GemSelectorPanel
|
||||||
from bauh.view.qt.systray import TrayIcon
|
from bauh.view.qt.systray import TrayIcon
|
||||||
from bauh.view.qt.window import ManageWindow
|
from bauh.view.qt.window import ManageWindow
|
||||||
|
|
||||||
@@ -33,37 +34,48 @@ context = ApplicationContext(i18n=i18n,
|
|||||||
disk_loader_factory=DefaultDiskCacheLoaderFactory(disk_cache_enabled=args.disk_cache),
|
disk_loader_factory=DefaultDiskCacheLoaderFactory(disk_cache_enabled=args.disk_cache),
|
||||||
logger=logger)
|
logger=logger)
|
||||||
|
|
||||||
managers = extensions.load_managers(context=context, locale=args.locale)
|
|
||||||
|
|
||||||
|
|
||||||
manager = GenericSoftwareManager(managers, context=context, app_args=args)
|
|
||||||
manager.prepare()
|
|
||||||
|
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
app.setApplicationName(__app_name__)
|
app.setApplicationName(__app_name__)
|
||||||
app.setApplicationVersion(__version__)
|
app.setApplicationVersion(__version__)
|
||||||
app.setWindowIcon(QIcon(resource.get_path('img/logo.svg')))
|
app.setWindowIcon(QIcon(resource.get_path('img/logo.svg')))
|
||||||
|
|
||||||
manage_window = ManageWindow(i18n=i18n,
|
user_config = config.read()
|
||||||
manager=manager,
|
|
||||||
icon_cache=icon_cache,
|
|
||||||
disk_cache=args.disk_cache,
|
|
||||||
download_icons=bool(args.download_icons),
|
|
||||||
screen_size=app.primaryScreen().size(),
|
|
||||||
suggestions=args.sugs,
|
|
||||||
display_limit=args.max_displayed)
|
|
||||||
|
|
||||||
if args.tray:
|
if not user_config.gems:
|
||||||
trayIcon = TrayIcon(locale_keys=i18n,
|
managers = gems.load_managers(context=context, locale=args.locale)
|
||||||
manager=manager,
|
|
||||||
manage_window=manage_window,
|
|
||||||
check_interval=args.check_interval,
|
|
||||||
update_notification=bool(args.update_notification))
|
|
||||||
manage_window.tray_icon = trayIcon
|
|
||||||
trayIcon.show()
|
|
||||||
else:
|
else:
|
||||||
manage_window.refresh_apps()
|
managers = gems.load_managers(context=context, locale=args.locale, names=user_config.gems)
|
||||||
manage_window.show()
|
|
||||||
|
enabled_managers = [m for m in managers if m.is_enabled()]
|
||||||
|
|
||||||
|
if not user_config.gems and enabled_managers:
|
||||||
|
gem_panel = GemSelectorPanel(enabled_managers, i18n, boot=True)
|
||||||
|
gem_panel.show()
|
||||||
|
else:
|
||||||
|
manager = GenericSoftwareManager(enabled_managers, context=context, app_args=args)
|
||||||
|
manager.prepare()
|
||||||
|
|
||||||
|
manage_window = ManageWindow(i18n=i18n,
|
||||||
|
manager=manager,
|
||||||
|
icon_cache=icon_cache,
|
||||||
|
disk_cache=args.disk_cache,
|
||||||
|
download_icons=bool(args.download_icons),
|
||||||
|
screen_size=app.primaryScreen().size(),
|
||||||
|
suggestions=args.sugs,
|
||||||
|
display_limit=args.max_displayed)
|
||||||
|
|
||||||
|
if args.tray:
|
||||||
|
trayIcon = TrayIcon(locale_keys=i18n,
|
||||||
|
manager=manager,
|
||||||
|
manage_window=manage_window,
|
||||||
|
check_interval=args.check_interval,
|
||||||
|
update_notification=bool(args.update_notification))
|
||||||
|
manage_window.tray_icon = trayIcon
|
||||||
|
trayIcon.show()
|
||||||
|
else:
|
||||||
|
manage_window.refresh_apps()
|
||||||
|
manage_window.show()
|
||||||
|
|
||||||
|
cache_cleaner.start()
|
||||||
|
|
||||||
cache_cleaner.start()
|
|
||||||
sys.exit(app.exec_())
|
sys.exit(app.exec_())
|
||||||
|
|||||||
0
bauh/commons/__init__.py
Normal file
0
bauh/commons/__init__.py
Normal file
11
bauh/commons/html.py
Normal file
11
bauh/commons/html.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
HTML_RE = re.compile(r'<[^>]+>')
|
||||||
|
|
||||||
|
|
||||||
|
def strip_html(string: str):
|
||||||
|
return HTML_RE.sub('', string)
|
||||||
|
|
||||||
|
|
||||||
|
def bold(text: str) -> str:
|
||||||
|
return '<span style="font-weight: bold">{}</span>'.format(text)
|
||||||
3
bauh/commons/resource.py
Normal file
3
bauh/commons/resource.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
def get_path(resource_path, root_dir: str):
|
||||||
|
return root_dir + '/resources/' + resource_path
|
||||||
135
bauh/commons/system.py
Normal file
135
bauh/commons/system.py
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
from subprocess import PIPE
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
# default environment variables for subprocesses.
|
||||||
|
from bauh.api.abstract.handler import ProcessWatcher
|
||||||
|
|
||||||
|
GLOBAL_INTERPRETER_PATH = ':'.join(os.getenv('PATH').split(':')[1:])
|
||||||
|
USE_GLOBAL_INTERPRETER = bool(os.getenv('VIRTUAL_ENV'))
|
||||||
|
|
||||||
|
|
||||||
|
def gen_env(global_interpreter: bool) -> dict:
|
||||||
|
res = {'LANG': 'en'}
|
||||||
|
|
||||||
|
if global_interpreter: # to avoid subprocess calls to the virtualenv python interpreter instead of the global one.
|
||||||
|
res['PATH'] = GLOBAL_INTERPRETER_PATH
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
class SystemProcess:
|
||||||
|
|
||||||
|
"""
|
||||||
|
Represents a system process being executed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, subproc: subprocess.Popen, success_phrase: str = None, wrong_error_phrase: str = '[sudo] password for', check_error_output: bool = True):
|
||||||
|
self.subproc = subproc
|
||||||
|
self.success_phrase = success_phrase
|
||||||
|
self.wrong_error_phrase = wrong_error_phrase
|
||||||
|
self.check_error_output = check_error_output
|
||||||
|
|
||||||
|
def wait(self):
|
||||||
|
self.subproc.wait()
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessHandler:
|
||||||
|
|
||||||
|
"""
|
||||||
|
It handles a process execution and notifies a specified watcher.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, watcher: ProcessWatcher = None):
|
||||||
|
self.watcher = watcher
|
||||||
|
|
||||||
|
def _notify_watcher(self, msg: str):
|
||||||
|
if self.watcher:
|
||||||
|
self.watcher.print(msg)
|
||||||
|
|
||||||
|
def handle(self, process: SystemProcess) -> bool:
|
||||||
|
self._notify_watcher(' '.join(process.subproc.args) + '\n')
|
||||||
|
|
||||||
|
already_succeeded = False
|
||||||
|
|
||||||
|
for output in process.subproc.stdout:
|
||||||
|
line = output.decode().strip()
|
||||||
|
if line:
|
||||||
|
self._notify_watcher(line)
|
||||||
|
|
||||||
|
if process.success_phrase and process.success_phrase in line:
|
||||||
|
already_succeeded = True
|
||||||
|
|
||||||
|
if already_succeeded:
|
||||||
|
return True
|
||||||
|
|
||||||
|
for output in process.subproc.stderr:
|
||||||
|
line = output.decode().strip()
|
||||||
|
if line:
|
||||||
|
self._notify_watcher(line)
|
||||||
|
|
||||||
|
if process.check_error_output:
|
||||||
|
if process.wrong_error_phrase and process.wrong_error_phrase in line:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
return process.subproc.returncode is None or process.subproc.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
def run_cmd(cmd: str, expected_code: int = 0, ignore_return_code: bool = False, print_error: bool = True,
|
||||||
|
cwd: str = '.', global_interpreter: bool = USE_GLOBAL_INTERPRETER) -> str:
|
||||||
|
"""
|
||||||
|
runs a given command and returns its default output
|
||||||
|
:param cmd:
|
||||||
|
:param expected_code:
|
||||||
|
:param ignore_return_code:
|
||||||
|
:param print_error:
|
||||||
|
:param global_interpreter
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
args = {
|
||||||
|
"shell": True,
|
||||||
|
"stdout": PIPE,
|
||||||
|
"env": gen_env(global_interpreter),
|
||||||
|
'cwd': cwd
|
||||||
|
}
|
||||||
|
|
||||||
|
if not print_error:
|
||||||
|
args["stderr"] = subprocess.DEVNULL
|
||||||
|
|
||||||
|
res = subprocess.run(cmd, **args)
|
||||||
|
return res.stdout.decode() if ignore_return_code or res.returncode == expected_code else None
|
||||||
|
|
||||||
|
|
||||||
|
def new_subprocess(cmd: List[str], cwd: str = '.', shell: bool = False, stdin = None,
|
||||||
|
global_interpreter: bool = USE_GLOBAL_INTERPRETER) -> subprocess.Popen:
|
||||||
|
args = {
|
||||||
|
"stdout": PIPE,
|
||||||
|
"stderr": PIPE,
|
||||||
|
"cwd": cwd,
|
||||||
|
"shell": shell,
|
||||||
|
"env": gen_env(global_interpreter)
|
||||||
|
}
|
||||||
|
|
||||||
|
if input:
|
||||||
|
args['stdin'] = stdin
|
||||||
|
|
||||||
|
return subprocess.Popen(cmd, **args)
|
||||||
|
|
||||||
|
|
||||||
|
def new_root_subprocess(cmd: List[str], root_password: str, cwd: str = '.',
|
||||||
|
global_interpreter: bool = USE_GLOBAL_INTERPRETER) -> subprocess.Popen:
|
||||||
|
pwdin, final_cmd = None, []
|
||||||
|
|
||||||
|
if root_password is not None:
|
||||||
|
final_cmd.extend(['sudo', '-S'])
|
||||||
|
pwdin = new_subprocess(['echo', root_password], global_interpreter=global_interpreter).stdout
|
||||||
|
|
||||||
|
final_cmd.extend(cmd)
|
||||||
|
return subprocess.Popen(final_cmd, stdin=pwdin, stdout=PIPE, stderr=PIPE, cwd=cwd)
|
||||||
|
|
||||||
|
|
||||||
|
def notify_user(msg: str, app_name: str, icon_path: str):
|
||||||
|
os.system("notify-send -a {} {} '{}'".format(app_name, "-i {}".format(icon_path) if icon_path else '', msg))
|
||||||
33
bauh/core/config.py
Normal file
33
bauh/core/config.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from bauh import __app_name__
|
||||||
|
from bauh.api.constants import HOME_PATH
|
||||||
|
|
||||||
|
CONFIG_PATH = '{}/.config/{}'.format(HOME_PATH, __app_name__)
|
||||||
|
FILE_PATH = '{}/config.json'.format(CONFIG_PATH)
|
||||||
|
|
||||||
|
|
||||||
|
class Configuration:
|
||||||
|
|
||||||
|
def __init__(self, gems: List[str]):
|
||||||
|
self.gems = gems
|
||||||
|
|
||||||
|
|
||||||
|
def read() -> Configuration:
|
||||||
|
if os.path.exists(FILE_PATH):
|
||||||
|
with open(FILE_PATH) as f:
|
||||||
|
config_file = f.read()
|
||||||
|
|
||||||
|
return Configuration(**json.loads(config_file))
|
||||||
|
|
||||||
|
return Configuration(gems=None)
|
||||||
|
|
||||||
|
|
||||||
|
def save(config: Configuration):
|
||||||
|
Path(CONFIG_PATH).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
with open(FILE_PATH, 'w+') as f:
|
||||||
|
f.write(json.dumps(config.__dict__, indent=2))
|
||||||
@@ -2,10 +2,10 @@ from argparse import Namespace
|
|||||||
from threading import Thread
|
from threading import Thread
|
||||||
from typing import List, Set, Type
|
from typing import List, Set, Type
|
||||||
|
|
||||||
from bauh_api.abstract.controller import SoftwareManager, SearchResult, ApplicationContext
|
from bauh.api.abstract.controller import SoftwareManager, SearchResult, ApplicationContext
|
||||||
from bauh_api.abstract.disk import DiskCacheLoader
|
from bauh.api.abstract.disk import DiskCacheLoader
|
||||||
from bauh_api.abstract.handler import ProcessWatcher
|
from bauh.api.abstract.handler import ProcessWatcher
|
||||||
from bauh_api.abstract.model import SoftwarePackage, PackageUpdate, PackageHistory, PackageSuggestion, PackageAction
|
from bauh.api.abstract.model import SoftwarePackage, PackageUpdate, PackageHistory, PackageSuggestion, PackageAction
|
||||||
|
|
||||||
SUGGESTIONS_LIMIT = 5
|
SUGGESTIONS_LIMIT = 5
|
||||||
|
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
import inspect
|
|
||||||
import os
|
|
||||||
import pkgutil
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from bauh_api.abstract.controller import SoftwareManager, ApplicationContext
|
|
||||||
|
|
||||||
from bauh import __app_name__
|
|
||||||
from bauh.util import util
|
|
||||||
|
|
||||||
ignore_modules = {'{}_api'.format(__app_name__), '{}_commons'.format(__app_name__)}
|
|
||||||
ext_pattern = '{}_'.format(__app_name__)
|
|
||||||
|
|
||||||
|
|
||||||
def find_manager(member):
|
|
||||||
if not isinstance(member, str):
|
|
||||||
if inspect.isclass(member) and inspect.getmro(member)[1].__name__ == 'SoftwareManager':
|
|
||||||
return member
|
|
||||||
elif inspect.ismodule(member) and member.__name__ not in ignore_modules:
|
|
||||||
for name, mod in inspect.getmembers(member):
|
|
||||||
manager_found = find_manager(mod)
|
|
||||||
if manager_found:
|
|
||||||
return manager_found
|
|
||||||
|
|
||||||
|
|
||||||
def load_managers(locale: str, context: ApplicationContext) -> List[SoftwareManager]:
|
|
||||||
managers = []
|
|
||||||
|
|
||||||
for m in pkgutil.iter_modules():
|
|
||||||
if m.ispkg and m.name and m.name not in ignore_modules and m.name.startswith(ext_pattern):
|
|
||||||
module = pkgutil.find_loader(m.name).load_module()
|
|
||||||
|
|
||||||
if hasattr(module, 'controller'):
|
|
||||||
manager_class = find_manager(module.controller)
|
|
||||||
|
|
||||||
if manager_class:
|
|
||||||
locale_path = '{}/resources/locale'.format(module.__path__[0])
|
|
||||||
|
|
||||||
if os.path.exists(locale_path):
|
|
||||||
context.i18n.update(util.get_locale_keys(locale, locale_path))
|
|
||||||
|
|
||||||
managers.append(manager_class(context=context))
|
|
||||||
|
|
||||||
return managers
|
|
||||||
40
bauh/core/gems.py
Normal file
40
bauh/core/gems.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import inspect
|
||||||
|
import os
|
||||||
|
import pkgutil
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from bauh import ROOT_DIR
|
||||||
|
from bauh.api.abstract.controller import SoftwareManager, ApplicationContext
|
||||||
|
from bauh.util import util
|
||||||
|
|
||||||
|
|
||||||
|
def find_manager(member):
|
||||||
|
if not isinstance(member, str):
|
||||||
|
if inspect.isclass(member) and inspect.getmro(member)[1].__name__ == 'SoftwareManager':
|
||||||
|
return member
|
||||||
|
elif inspect.ismodule(member):
|
||||||
|
for name, mod in inspect.getmembers(member):
|
||||||
|
manager_found = find_manager(mod)
|
||||||
|
if manager_found:
|
||||||
|
return manager_found
|
||||||
|
|
||||||
|
|
||||||
|
def load_managers(locale: str, context: ApplicationContext, names: List[str] = None) -> List[SoftwareManager]:
|
||||||
|
managers = []
|
||||||
|
|
||||||
|
for f in os.scandir(ROOT_DIR + '/gems'):
|
||||||
|
if f.is_dir() and f.name != '__pycache__' and (not names or f.name in names):
|
||||||
|
module = pkgutil.find_loader('bauh.gems.{}.controller'.format(f.name)).load_module()
|
||||||
|
|
||||||
|
manager_class = find_manager(module)
|
||||||
|
|
||||||
|
if manager_class:
|
||||||
|
locale_path = '{}/resources/locale'.format(f.path)
|
||||||
|
|
||||||
|
if os.path.exists(locale_path):
|
||||||
|
context.i18n.update(util.get_locale_keys(locale, locale_path))
|
||||||
|
|
||||||
|
managers.append(manager_class(context=context))
|
||||||
|
|
||||||
|
return managers
|
||||||
|
|
||||||
0
bauh/gems/__init__.py
Normal file
0
bauh/gems/__init__.py
Normal file
4
bauh/gems/arch/__init__.py
Normal file
4
bauh/gems/arch/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import os
|
||||||
|
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
BUILD_DIR = '/tmp/bauh/aur'
|
||||||
|
|
||||||
28
bauh/gems/arch/aur.py
Normal file
28
bauh/gems/arch/aur.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import re
|
||||||
|
from typing import Set, List
|
||||||
|
|
||||||
|
from bauh.api.http import HttpClient
|
||||||
|
|
||||||
|
URL_INFO = 'https://aur.archlinux.org/rpc/?v=5&type=info&'
|
||||||
|
URL_SEARCH = 'https://aur.archlinux.org/rpc/?v=5&type=search&arg='
|
||||||
|
|
||||||
|
|
||||||
|
def map_pkgbuild(pkgbuild: str) -> dict:
|
||||||
|
return {attr: val.replace('"', '').replace("'", '').replace('(', '').replace(')', '') for attr, val in re.findall(r'\n(\w+)=(.+)', pkgbuild)}
|
||||||
|
|
||||||
|
|
||||||
|
class AURClient:
|
||||||
|
|
||||||
|
def __init__(self, http_client: HttpClient):
|
||||||
|
self.http_client = http_client
|
||||||
|
self.names_index = set()
|
||||||
|
|
||||||
|
def search(self, words: str) -> dict:
|
||||||
|
return self.http_client.get_json(URL_SEARCH + words)
|
||||||
|
|
||||||
|
def get_info(self, names: Set[str]) -> List[dict]:
|
||||||
|
res = self.http_client.get_json(URL_INFO + self._map_names_as_queries(names))
|
||||||
|
return res['results'] if res and res.get('results') else []
|
||||||
|
|
||||||
|
def _map_names_as_queries(self, names) -> str:
|
||||||
|
return '&'.join(['arg[{}]={}'.format(i, n) for i, n in enumerate(names)])
|
||||||
28
bauh/gems/arch/confirmation.py
Normal file
28
bauh/gems/arch/confirmation.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
from typing import Set
|
||||||
|
|
||||||
|
from bauh.api.abstract.handler import ProcessWatcher
|
||||||
|
from bauh.api.abstract.view import MultipleSelectComponent, InputOption
|
||||||
|
|
||||||
|
from bauh.commons.html import bold
|
||||||
|
|
||||||
|
|
||||||
|
def request_optional_deps(pkgname: str, pkg_mirrors: dict, watcher: ProcessWatcher, i18n: dict) -> Set[str]:
|
||||||
|
view_opts = MultipleSelectComponent(label='',
|
||||||
|
options=[InputOption('{} ( {} )'.format(p, m.upper()), p) for p, m in
|
||||||
|
pkg_mirrors.items()])
|
||||||
|
install = watcher.request_confirmation(title=i18n['arch.install.optdeps.request.title'],
|
||||||
|
body='<p>{}</p>'.format(i18n['arch.install.optdeps.request.body'].format(bold(pkgname)) + ':'),
|
||||||
|
components=[view_opts],
|
||||||
|
confirmation_label=i18n['install'],
|
||||||
|
deny_label=i18n['cancel'])
|
||||||
|
|
||||||
|
if install:
|
||||||
|
return {o.value for o in view_opts.values}
|
||||||
|
|
||||||
|
|
||||||
|
def request_install_missing_deps(pkgname: str, pkg_mirrors: dict, watcher: ProcessWatcher, i18n: dict) -> bool:
|
||||||
|
deps_str = ''.join(['<br/><span style="font-weight:bold"> - {} ( {} )</span>'.format(d, m.upper()) for d, m in pkg_mirrors.items()])
|
||||||
|
msg = '<p>{}</p>'.format(i18n['arch.missing_deps.body'].format('<span style="font-weight: bold">{}</span>'.format(pkgname) + ':<br/>' + deps_str))
|
||||||
|
msg += i18n['ask.continue']
|
||||||
|
|
||||||
|
return watcher.request_confirmation(i18n['arch.missing_deps.title'], msg)
|
||||||
570
bauh/gems/arch/controller.py
Normal file
570
bauh/gems/arch/controller.py
Normal file
@@ -0,0 +1,570 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from threading import Thread
|
||||||
|
from typing import List, Set, Type
|
||||||
|
|
||||||
|
from bauh.api.abstract.controller import SearchResult, SoftwareManager, ApplicationContext
|
||||||
|
from bauh.api.abstract.disk import DiskCacheLoader
|
||||||
|
from bauh.api.abstract.handler import ProcessWatcher
|
||||||
|
from bauh.api.abstract.model import PackageUpdate, PackageHistory, SoftwarePackage, PackageSuggestion
|
||||||
|
from bauh.api.abstract.view import MessageType
|
||||||
|
from bauh.commons.system import SystemProcess, ProcessHandler, new_subprocess, run_cmd, new_root_subprocess
|
||||||
|
|
||||||
|
from bauh.gems.arch import BUILD_DIR, aur, pacman, makepkg, pkgbuild, message, confirmation, disk, git, suggestions
|
||||||
|
from bauh.gems.arch.aur import AURClient
|
||||||
|
from bauh.gems.arch.mapper import ArchDataMapper
|
||||||
|
from bauh.gems.arch.model import ArchPackage
|
||||||
|
from bauh.commons.html import bold
|
||||||
|
from bauh.gems.arch.worker import AURIndexUpdater, ArchDiskCacheUpdater
|
||||||
|
|
||||||
|
URL_GIT = 'https://aur.archlinux.org/{}.git'
|
||||||
|
URL_PKG_DOWNLOAD = 'https://aur.archlinux.org/cgit/aur.git/snapshot/{}.tar.gz'
|
||||||
|
URL_SRC_INFO = 'https://aur.archlinux.org/cgit/aur.git/plain/.SRCINFO?h='
|
||||||
|
|
||||||
|
RE_SPLIT_VERSION = re.compile(r'(=|>|<)')
|
||||||
|
|
||||||
|
|
||||||
|
class ArchManager(SoftwareManager):
|
||||||
|
|
||||||
|
def __init__(self, context: ApplicationContext):
|
||||||
|
super(ArchManager, self).__init__(context=context)
|
||||||
|
self.aur_cache = context.cache_factory.new()
|
||||||
|
# context.disk_loader_factory.map(ArchPackage, self.aur_cache) TODO
|
||||||
|
|
||||||
|
self.mapper = ArchDataMapper(http_client=context.http_client)
|
||||||
|
self.i18n = context.i18n
|
||||||
|
self.aur_client = AURClient(context.http_client)
|
||||||
|
self.names_index = {}
|
||||||
|
self.aur_index_updater = AURIndexUpdater(context, self)
|
||||||
|
self.dcache_updater = ArchDiskCacheUpdater(context.logger)
|
||||||
|
self.logger = context.logger
|
||||||
|
|
||||||
|
def _upgrade_search_result(self, apidata: dict, installed_pkgs: dict, downgrade_enabled: bool, res: SearchResult, disk_loader: DiskCacheLoader):
|
||||||
|
app = self.mapper.map_api_data(apidata, installed_pkgs['not_signed'])
|
||||||
|
app.downgrade_enabled = downgrade_enabled
|
||||||
|
|
||||||
|
if app.installed:
|
||||||
|
res.installed.append(app)
|
||||||
|
|
||||||
|
if disk_loader:
|
||||||
|
disk_loader.fill(app)
|
||||||
|
else:
|
||||||
|
res.new.append(app)
|
||||||
|
|
||||||
|
Thread(target=self.mapper.fill_package_build, args=(app,)).start()
|
||||||
|
|
||||||
|
def search(self, words: str, disk_loader: DiskCacheLoader, limit: int = -1) -> SearchResult:
|
||||||
|
downgrade_enabled = git.is_enabled()
|
||||||
|
res = SearchResult([], [], 0)
|
||||||
|
|
||||||
|
installed = {}
|
||||||
|
read_installed = Thread(target=lambda: installed.update(pacman.list_and_map_installed()))
|
||||||
|
read_installed.start()
|
||||||
|
|
||||||
|
api_res = self.aur_client.search(words)
|
||||||
|
|
||||||
|
if api_res and api_res.get('results'):
|
||||||
|
read_installed.join()
|
||||||
|
|
||||||
|
for pkgdata in api_res['results']:
|
||||||
|
self._upgrade_search_result(pkgdata, installed, downgrade_enabled, res, disk_loader)
|
||||||
|
|
||||||
|
else: # if there are no results from the API (it could be because there were too many), tries the names index:
|
||||||
|
if self.names_index:
|
||||||
|
|
||||||
|
to_query = set()
|
||||||
|
for norm_name, real_name in self.names_index.items():
|
||||||
|
if words in norm_name:
|
||||||
|
to_query.add(real_name)
|
||||||
|
|
||||||
|
if len(to_query) == 25:
|
||||||
|
break
|
||||||
|
|
||||||
|
pkgsinfo = self.aur_client.get_info(to_query)
|
||||||
|
|
||||||
|
if pkgsinfo:
|
||||||
|
read_installed.join()
|
||||||
|
|
||||||
|
for pkgdata in pkgsinfo:
|
||||||
|
self._upgrade_search_result(pkgdata, installed, res)
|
||||||
|
|
||||||
|
res.total = len(res.installed) + len(res.new)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def _fill_aur_pkgs(self, not_signed: dict, apps: list, disk_loader: DiskCacheLoader):
|
||||||
|
downgrade_enabled = git.is_enabled()
|
||||||
|
pkgsinfo = self.aur_client.get_info(not_signed.keys())
|
||||||
|
|
||||||
|
if pkgsinfo:
|
||||||
|
for pkgdata in pkgsinfo:
|
||||||
|
app = self.mapper.map_api_data(pkgdata, not_signed)
|
||||||
|
app.downgrade_enabled = downgrade_enabled
|
||||||
|
if disk_loader:
|
||||||
|
disk_loader.fill(app)
|
||||||
|
|
||||||
|
apps.append(app)
|
||||||
|
|
||||||
|
def _fill_mirror_pkgs(self, mirrors: dict, apps: list):
|
||||||
|
# TODO
|
||||||
|
for name, data in mirrors.items():
|
||||||
|
app = ArchPackage(name=name, version=data.get('version'), latest_version=data.get('version'), description=data.get('description'))
|
||||||
|
app.installed = True
|
||||||
|
app.mirror = '' # TODO
|
||||||
|
app.update = False # TODO
|
||||||
|
apps.append(app)
|
||||||
|
|
||||||
|
def read_installed(self, disk_loader: DiskCacheLoader, limit: int = -1, only_apps: bool = False, pkg_types: Set[Type[SoftwarePackage]] = None) -> SearchResult:
|
||||||
|
installed = pacman.list_and_map_installed()
|
||||||
|
|
||||||
|
apps = []
|
||||||
|
if installed and installed['not_signed']:
|
||||||
|
self._fill_aur_pkgs(installed['not_signed'], apps, disk_loader)
|
||||||
|
|
||||||
|
return SearchResult(apps, None, len(apps))
|
||||||
|
|
||||||
|
def downgrade(self, pkg: ArchPackage, root_password: str, watcher: ProcessWatcher) -> bool:
|
||||||
|
|
||||||
|
handler = ProcessHandler(watcher)
|
||||||
|
app_build_dir = '{}/build_{}'.format(BUILD_DIR, int(time.time()))
|
||||||
|
watcher.change_progress(5)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not os.path.exists(app_build_dir):
|
||||||
|
build_dir = handler.handle(SystemProcess(new_subprocess(['mkdir', '-p', app_build_dir])))
|
||||||
|
|
||||||
|
if build_dir:
|
||||||
|
watcher.change_progress(10)
|
||||||
|
watcher.change_substatus(self.i18n['arch.clone'].format(bold(pkg.name)))
|
||||||
|
clone = handler.handle(SystemProcess(subproc=new_subprocess(['git', 'clone', URL_GIT.format(pkg.name)], cwd=app_build_dir), check_error_output=False))
|
||||||
|
watcher.change_progress(30)
|
||||||
|
if clone:
|
||||||
|
watcher.change_substatus(self.i18n['arch.downgrade.reading_commits'])
|
||||||
|
clone_path = '{}/{}'.format(app_build_dir, pkg.name)
|
||||||
|
pkgbuild_path = '{}/PKGBUILD'.format(clone_path)
|
||||||
|
|
||||||
|
commits = run_cmd("git log", cwd=clone_path)
|
||||||
|
watcher.change_progress(40)
|
||||||
|
|
||||||
|
if commits:
|
||||||
|
commit_list = re.findall(r'commit (.+)\n', commits)
|
||||||
|
if commit_list:
|
||||||
|
if len(commit_list) > 1:
|
||||||
|
for idx in range(1, len(commit_list)):
|
||||||
|
commit = commit_list[idx]
|
||||||
|
with open(pkgbuild_path) as f:
|
||||||
|
pkgdict = aur.map_pkgbuild(f.read())
|
||||||
|
|
||||||
|
if not handler.handle(SystemProcess(subproc=new_subprocess(['git', 'reset', '--hard', commit], cwd=clone_path), check_error_output=False)):
|
||||||
|
watcher.print('Could not downgrade anymore. Aborting...')
|
||||||
|
return False
|
||||||
|
|
||||||
|
if '{}-{}'.format(pkgdict.get('pkgver'), pkgdict.get('pkgrel')) == pkg.version:
|
||||||
|
# current version found
|
||||||
|
watcher.change_substatus(self.i18n['arch.downgrade.version_found'])
|
||||||
|
break
|
||||||
|
|
||||||
|
watcher.change_substatus(self.i18n['arch.downgrade.install_older'])
|
||||||
|
return self._make_pkg(pkg.name, root_password, handler, app_build_dir, clone_path, dependency=False, skip_optdeps=True)
|
||||||
|
else:
|
||||||
|
watcher.show_message(title=self.i18n['arch.downgrade.error'],
|
||||||
|
body=self.i18n['arch.downgrade.impossible'].format(pkg.name),
|
||||||
|
type_=MessageType.ERROR)
|
||||||
|
return False
|
||||||
|
|
||||||
|
watcher.show_message(title=self.i18n['error'], body=self.i18n['arch.downgrade.no_commits'], type_=MessageType.ERROR)
|
||||||
|
return False
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if os.path.exists(app_build_dir):
|
||||||
|
handler.handle(SystemProcess(subproc=new_subprocess(['rm', '-rf', app_build_dir])))
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def clean_cache_for(self, pkg: ArchPackage):
|
||||||
|
if os.path.exists(pkg.get_disk_cache_path()):
|
||||||
|
shutil.rmtree(pkg.get_disk_cache_path())
|
||||||
|
|
||||||
|
def update(self, pkg: ArchPackage, root_password: str, watcher: ProcessWatcher) -> bool:
|
||||||
|
return self.install(pkg=pkg, root_password=root_password, watcher=watcher, skip_optdeps=True)
|
||||||
|
|
||||||
|
def _uninstall(self, pkg_name: str, root_password: str, handler: ProcessHandler) -> bool:
|
||||||
|
return handler.handle(SystemProcess(new_root_subprocess(['pacman', '-R', pkg_name, '--noconfirm'], root_password)))
|
||||||
|
|
||||||
|
def uninstall(self, pkg: ArchPackage, root_password: str, watcher: ProcessWatcher) -> bool:
|
||||||
|
handler = ProcessHandler(watcher)
|
||||||
|
|
||||||
|
watcher.change_progress(10)
|
||||||
|
info = pacman.get_info_dict(pkg.name)
|
||||||
|
watcher.change_progress(50)
|
||||||
|
|
||||||
|
if info.get('required by'):
|
||||||
|
pkname = '"{}"'.format(pkg.name)
|
||||||
|
msg = '{}:\n\n{}\n\n{}'.format(self.i18n['arch.uninstall.required_by'].format(pkname), info['required by'], self.i18n['arch.uninstall.required_by.advice'].format(pkname))
|
||||||
|
watcher.show_message(title=self.i18n['error'], body=msg, type_=MessageType.WARNING)
|
||||||
|
return False
|
||||||
|
|
||||||
|
uninstalled = self._uninstall(pkg.name, root_password, handler)
|
||||||
|
watcher.change_progress(100)
|
||||||
|
return uninstalled
|
||||||
|
|
||||||
|
def get_managed_types(self) -> Set["type"]:
|
||||||
|
return {ArchPackage}
|
||||||
|
|
||||||
|
def get_info(self, pkg: ArchPackage) -> dict:
|
||||||
|
if pkg.installed:
|
||||||
|
t = Thread(target=self.mapper.fill_package_build, args=(pkg,))
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
info = pacman.get_info_dict(pkg.name)
|
||||||
|
|
||||||
|
t.join()
|
||||||
|
|
||||||
|
if pkg.pkgbuild:
|
||||||
|
info['13_pkg_build'] = pkg.pkgbuild
|
||||||
|
|
||||||
|
return info
|
||||||
|
else:
|
||||||
|
info = {
|
||||||
|
'01_id': pkg.id,
|
||||||
|
'02_name': pkg.name,
|
||||||
|
'03_version': pkg.version,
|
||||||
|
'04_popularity': pkg.popularity,
|
||||||
|
'05_votes': pkg.votes,
|
||||||
|
'06_package_base': pkg.package_base,
|
||||||
|
'07_maintainer': pkg.maintainer,
|
||||||
|
'08_first_submitted': pkg.first_submitted,
|
||||||
|
'09_last_modified': pkg.last_modified,
|
||||||
|
'10_url': pkg.url_download,
|
||||||
|
}
|
||||||
|
|
||||||
|
res_srcinfo = self.context.http_client.get(URL_SRC_INFO + pkg.name)
|
||||||
|
|
||||||
|
if res_srcinfo and res_srcinfo.text:
|
||||||
|
info['11_dependson'] = ' '.join(pkgbuild.read_depends_on(res_srcinfo.text))
|
||||||
|
info['12_optdepends'] = ' '.join(pkgbuild.read_optdeps(res_srcinfo.text))
|
||||||
|
|
||||||
|
if pkg.pkgbuild:
|
||||||
|
info['13_pkg_build'] = pkg.pkgbuild
|
||||||
|
else:
|
||||||
|
info['11_pkg_build_url'] = pkg.get_pkg_build_url()
|
||||||
|
|
||||||
|
return info
|
||||||
|
|
||||||
|
def get_history(self, pkg: ArchPackage) -> PackageHistory:
|
||||||
|
temp_dir = '{}/build_{}'.format(BUILD_DIR, int(time.time()))
|
||||||
|
|
||||||
|
try:
|
||||||
|
Path(temp_dir).mkdir(parents=True)
|
||||||
|
run_cmd('git clone ' + URL_GIT.format(pkg.name), print_error=False, cwd=temp_dir)
|
||||||
|
|
||||||
|
clone_path = '{}/{}'.format(temp_dir, pkg.name)
|
||||||
|
pkgbuild_path = '{}/PKGBUILD'.format(clone_path)
|
||||||
|
|
||||||
|
commits = git.list_commits(clone_path)
|
||||||
|
|
||||||
|
if commits:
|
||||||
|
history, status_idx = [], 0
|
||||||
|
|
||||||
|
for idx, commit in enumerate(commits):
|
||||||
|
with open(pkgbuild_path) as f:
|
||||||
|
pkgdict = aur.map_pkgbuild(f.read())
|
||||||
|
|
||||||
|
if '{}-{}'.format(pkgdict.get('pkgver'), pkgdict.get('pkgrel')) == pkg.version:
|
||||||
|
status_idx = idx
|
||||||
|
|
||||||
|
history.append({'1_version': pkgdict['pkgver'], '2_release': pkgdict['pkgrel'],
|
||||||
|
'3_date': commit['date']}) # the number prefix is to ensure the rendering order
|
||||||
|
|
||||||
|
if idx + 1 < len(commits):
|
||||||
|
if not run_cmd('git reset --hard ' + commits[idx + 1]['commit'], cwd=clone_path):
|
||||||
|
break
|
||||||
|
|
||||||
|
return PackageHistory(pkg=pkg, history=history, pkg_status_idx=status_idx)
|
||||||
|
finally:
|
||||||
|
if os.path.exists(temp_dir):
|
||||||
|
shutil.rmtree(temp_dir)
|
||||||
|
|
||||||
|
def _install_deps(self, deps: Set[str], pkg_mirrors: dict, root_password: str, handler: ProcessHandler, change_progress: bool = False) -> str:
|
||||||
|
"""
|
||||||
|
:param deps:
|
||||||
|
:param pkg_mirrors:
|
||||||
|
:param root_password:
|
||||||
|
:param handler:
|
||||||
|
:return: not installed dependency
|
||||||
|
"""
|
||||||
|
progress_increment = int(100 / len(deps))
|
||||||
|
progress = 0
|
||||||
|
self._update_progress(handler.watcher, 1, change_progress)
|
||||||
|
|
||||||
|
for pkgname in deps:
|
||||||
|
|
||||||
|
mirror = pkg_mirrors[pkgname]
|
||||||
|
handler.watcher.change_substatus(self.i18n['arch.install.dependency.install'].format(bold('{} ()'.format(pkgname, mirror))))
|
||||||
|
if mirror == 'aur':
|
||||||
|
installed = self._install_from_aur(pkgname=pkgname, root_password=root_password, handler=handler, dependency=True, change_progress=False)
|
||||||
|
else:
|
||||||
|
installed = self._install(pkgname=pkgname, root_password=root_password, handler=handler, install_file=None, mirror=mirror, change_progress=False)
|
||||||
|
|
||||||
|
if not installed:
|
||||||
|
return pkgname
|
||||||
|
|
||||||
|
progress += progress_increment
|
||||||
|
self._update_progress(handler.watcher, progress, change_progress)
|
||||||
|
|
||||||
|
self._update_progress(handler.watcher, 100, change_progress)
|
||||||
|
|
||||||
|
def _map_mirrors(self, pkgnames: Set[str]) -> dict:
|
||||||
|
pkg_mirrors = pacman.get_mirrors(pkgnames) # getting mirrors set
|
||||||
|
|
||||||
|
if len(pkgnames) != pkg_mirrors: # checking if any dep not found in the distro mirrors are from AUR
|
||||||
|
nomirrors = {p for p in pkgnames if p not in pkg_mirrors}
|
||||||
|
for pkginfo in self.aur_client.get_info(nomirrors):
|
||||||
|
if pkginfo.get('Name') in nomirrors:
|
||||||
|
pkg_mirrors[pkginfo['Name']] = 'aur'
|
||||||
|
|
||||||
|
return pkg_mirrors
|
||||||
|
|
||||||
|
def _make_pkg(self, pkgname: str, root_password: str, handler: ProcessHandler, build_dir: str, project_dir: str, dependency: bool, skip_optdeps: bool = False, change_progress: bool = True) -> bool:
|
||||||
|
|
||||||
|
self._update_progress(handler.watcher, 50, change_progress)
|
||||||
|
if not self._install_missings_deps(pkgname, root_password, handler, project_dir):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# building main package
|
||||||
|
handler.watcher.change_substatus(self.i18n['arch.building.package'].format(bold(pkgname)))
|
||||||
|
pkgbuilt = handler.handle(SystemProcess(new_subprocess(['makepkg', '-Acsmf'], cwd=project_dir)))
|
||||||
|
self._update_progress(handler.watcher, 65, change_progress)
|
||||||
|
|
||||||
|
if pkgbuilt:
|
||||||
|
gen_file = [fname for root, dirs, files in os.walk(build_dir) for fname in files if re.match(r'^{}-.+\.tar\.xz'.format(pkgname), fname)]
|
||||||
|
|
||||||
|
if not gen_file:
|
||||||
|
handler.watcher.print('Could not find generated .tar.xz file. Aborting...')
|
||||||
|
return False
|
||||||
|
|
||||||
|
install_file = '{}/{}'.format(project_dir, gen_file[0])
|
||||||
|
|
||||||
|
if self._install(pkgname=pkgname, root_password=root_password, mirror='aur', handler=handler, install_file=install_file, pkgdir=project_dir, change_progress=change_progress):
|
||||||
|
|
||||||
|
if dependency or skip_optdeps:
|
||||||
|
return True
|
||||||
|
|
||||||
|
handler.watcher.change_substatus(self.i18n['arch.optdeps.checking'].format(bold(pkgname)))
|
||||||
|
|
||||||
|
if self._install_optdeps(pkgname, root_password, handler, project_dir, change_progress=change_progress):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _install_missings_deps(self, pkgname: str, root_password: str, handler: ProcessHandler, pkgdir: str) -> bool:
|
||||||
|
handler.watcher.change_substatus(self.i18n['arch.checking.deps'].format(bold(pkgname)))
|
||||||
|
missing_deps = makepkg.check_missing_deps(pkgdir, handler.watcher)
|
||||||
|
|
||||||
|
if missing_deps:
|
||||||
|
depnames = {RE_SPLIT_VERSION.split(dep)[0] for dep in missing_deps}
|
||||||
|
dep_mirrors = self._map_mirrors(depnames)
|
||||||
|
|
||||||
|
for dep in depnames: # cheking if a dependency could not be found in any mirror
|
||||||
|
if dep not in dep_mirrors:
|
||||||
|
message.show_dep_not_found(dep, self.i18n, handler.watcher)
|
||||||
|
return False
|
||||||
|
|
||||||
|
handler.watcher.change_substatus(self.i18n['arch.missing_deps_found'].format(bold(pkgname)))
|
||||||
|
|
||||||
|
if not confirmation.request_install_missing_deps(pkgname, dep_mirrors, handler.watcher, self.i18n):
|
||||||
|
handler.watcher.print(self.i18n['action.cancelled'])
|
||||||
|
return False
|
||||||
|
|
||||||
|
dep_not_installed = self._install_deps(depnames, dep_mirrors, root_password, handler, change_progress=False)
|
||||||
|
|
||||||
|
if dep_not_installed:
|
||||||
|
message.show_dep_not_installed(handler.watcher, pkgname, dep_not_installed, self.i18n)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _install_optdeps(self, pkgname: str, root_password: str, handler: ProcessHandler, pkgdir: str, change_progress: bool = True) -> bool:
|
||||||
|
with open('{}/.SRCINFO'.format(pkgdir)) as f:
|
||||||
|
odeps = pkgbuild.read_optdeps(f.read())
|
||||||
|
|
||||||
|
if not odeps:
|
||||||
|
return True
|
||||||
|
|
||||||
|
installed = pacman.list_installed()
|
||||||
|
to_install = {d for d in odeps if d not in installed}
|
||||||
|
|
||||||
|
if not to_install:
|
||||||
|
return True
|
||||||
|
|
||||||
|
pkg_mirrors = self._map_mirrors(to_install)
|
||||||
|
|
||||||
|
if pkg_mirrors:
|
||||||
|
deps_to_install = confirmation.request_optional_deps(pkgname, pkg_mirrors, handler.watcher, self.i18n)
|
||||||
|
|
||||||
|
if not deps_to_install:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
dep_not_installed = self._install_deps(deps_to_install, pkg_mirrors, root_password, handler, change_progress=True)
|
||||||
|
|
||||||
|
if dep_not_installed:
|
||||||
|
message.show_optdep_not_installed(dep_not_installed, handler.watcher, self.i18n)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _install(self, pkgname: str, root_password: str, mirror: str, handler: ProcessHandler, install_file: str = None, pkgdir: str = '.', change_progress: bool = True):
|
||||||
|
check_install_output = []
|
||||||
|
pkgpath = install_file if install_file else pkgname
|
||||||
|
|
||||||
|
handler.watcher.change_substatus(self.i18n['arch.checking.conflicts'].format(bold(pkgname)))
|
||||||
|
|
||||||
|
for check_out in new_root_subprocess(['pacman', '-U', pkgpath], root_password=root_password, cwd=pkgdir).stdout:
|
||||||
|
if check_out:
|
||||||
|
check_install_output.append(check_out.decode())
|
||||||
|
|
||||||
|
self._update_progress(handler.watcher, 70, change_progress)
|
||||||
|
if check_install_output and 'conflict' in check_install_output[-1]:
|
||||||
|
conflicting_apps = [w[0] for w in re.findall(r'((\w|\-|\.)+)\s(and|are)', check_install_output[-1])]
|
||||||
|
conflict_msg = ' {} '.format(self.i18n['and']).join(conflicting_apps)
|
||||||
|
if not handler.watcher.request_confirmation(title=self.i18n['arch.install.conflict.popup.title'],
|
||||||
|
body=self.i18n['arch.install.conflict.popup.body'].format(conflict_msg)):
|
||||||
|
handler.watcher.print(self.i18n['action.cancelled'])
|
||||||
|
return False
|
||||||
|
else: # uninstall conflicts
|
||||||
|
self._update_progress(handler.watcher, 75, change_progress)
|
||||||
|
to_uninstall = [conflict for conflict in conflicting_apps if conflict != pkgname]
|
||||||
|
|
||||||
|
for conflict in to_uninstall:
|
||||||
|
handler.watcher.change_substatus(self.i18n['arch.uninstalling.conflict'].format(bold(conflict)))
|
||||||
|
if not self._uninstall(conflict, root_password, handler):
|
||||||
|
handler.watcher.show_message(title=self.i18n['error'],
|
||||||
|
body=self.i18n['arch.uninstalling.conflict.fail'].format('"{}"'.format(conflict)),
|
||||||
|
type_=MessageType.ERROR)
|
||||||
|
return False
|
||||||
|
|
||||||
|
handler.watcher.change_substatus(self.i18n['arch.installing.package'].format(bold(pkgname)))
|
||||||
|
self._update_progress(handler.watcher, 80, change_progress)
|
||||||
|
installed = handler.handle(pacman.install_as_process(pkgpath=pkgpath, root_password=root_password, aur=install_file is not None, pkgdir=pkgdir))
|
||||||
|
self._update_progress(handler.watcher, 95, change_progress)
|
||||||
|
|
||||||
|
if installed and self.context.disk_cache:
|
||||||
|
handler.watcher.change_substatus(self.i18n['status.caching_data'].format(bold(pkgname)))
|
||||||
|
disk.save(pkgname, mirror)
|
||||||
|
self._update_progress(handler.watcher, 100, change_progress)
|
||||||
|
|
||||||
|
return installed
|
||||||
|
|
||||||
|
def _update_progress(self, watcher: ProcessWatcher, val: int, change_progress: bool):
|
||||||
|
if change_progress:
|
||||||
|
watcher.change_progress(val)
|
||||||
|
|
||||||
|
def _install_from_aur(self, pkgname: str, root_password: str, handler: ProcessHandler, dependency: bool, skip_optdeps: bool = False, change_progress: bool = True) -> bool:
|
||||||
|
app_build_dir = '{}/build_{}'.format(BUILD_DIR, int(time.time()))
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not os.path.exists(app_build_dir):
|
||||||
|
build_dir = handler.handle(SystemProcess(new_subprocess(['mkdir', '-p', app_build_dir])))
|
||||||
|
self._update_progress(handler.watcher, 10, change_progress)
|
||||||
|
|
||||||
|
if build_dir:
|
||||||
|
file_url = URL_PKG_DOWNLOAD.format(pkgname)
|
||||||
|
file_name = file_url.split('/')[-1]
|
||||||
|
handler.watcher.change_substatus('{} {}'.format(self.i18n['arch.downloading.package'], bold(file_name)))
|
||||||
|
download = handler.handle(SystemProcess(new_subprocess(['wget', file_url], cwd=app_build_dir)))
|
||||||
|
|
||||||
|
if download:
|
||||||
|
self._update_progress(handler.watcher, 30, change_progress)
|
||||||
|
handler.watcher.change_substatus('{} {}'.format(self.i18n['arch.uncompressing.package'], bold(file_name)))
|
||||||
|
uncompress = handler.handle(SystemProcess(new_subprocess(['tar', 'xvzf', '{}.tar.gz'.format(pkgname)], cwd=app_build_dir)))
|
||||||
|
self._update_progress(handler.watcher, 40, change_progress)
|
||||||
|
|
||||||
|
if uncompress:
|
||||||
|
uncompress_dir = '{}/{}'.format(app_build_dir, pkgname)
|
||||||
|
return self._make_pkg(pkgname=pkgname,
|
||||||
|
root_password=root_password,
|
||||||
|
handler=handler,
|
||||||
|
build_dir=app_build_dir,
|
||||||
|
project_dir=uncompress_dir,
|
||||||
|
dependency=dependency,
|
||||||
|
skip_optdeps=skip_optdeps,
|
||||||
|
change_progress=change_progress)
|
||||||
|
finally:
|
||||||
|
if os.path.exists(app_build_dir):
|
||||||
|
handler.handle(SystemProcess(new_subprocess(['rm', '-rf', app_build_dir])))
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def install(self, pkg: ArchPackage, root_password: str, watcher: ProcessWatcher, skip_optdeps: bool = False) -> bool:
|
||||||
|
return self._install_from_aur(pkg.name, root_password, ProcessHandler(watcher), dependency=False, skip_optdeps=skip_optdeps)
|
||||||
|
|
||||||
|
def _is_wget_available(self):
|
||||||
|
try:
|
||||||
|
new_subprocess(['wget', '--version'])
|
||||||
|
return True
|
||||||
|
except FileNotFoundError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def is_enabled(self) -> bool:
|
||||||
|
try:
|
||||||
|
return pacman.is_enabled() and self._is_wget_available()
|
||||||
|
except FileNotFoundError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def is_downgrade_enabled(self) -> bool:
|
||||||
|
try:
|
||||||
|
new_subprocess(['git', '--version'])
|
||||||
|
return True
|
||||||
|
except FileNotFoundError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def cache_to_disk(self, pkg: ArchPackage, icon_bytes: bytes, only_icon: bool):
|
||||||
|
if self.context.disk_cache and pkg.supports_disk_cache():
|
||||||
|
pass
|
||||||
|
|
||||||
|
def requires_root(self, action: str, pkg: ArchPackage):
|
||||||
|
return action != 'search'
|
||||||
|
|
||||||
|
def prepare(self):
|
||||||
|
self.dcache_updater.start()
|
||||||
|
|
||||||
|
self.aur_index_updater.start()
|
||||||
|
|
||||||
|
def list_updates(self) -> List[PackageUpdate]:
|
||||||
|
return [PackageUpdate(app.id, app.latest_version, 'aur') for app in self.read_installed(disk_loader=None).installed if app.update]
|
||||||
|
|
||||||
|
def list_warnings(self) -> List[str]:
|
||||||
|
warnings = []
|
||||||
|
|
||||||
|
if not pacman.is_enabled():
|
||||||
|
warnings.append(self.i18n['arch.warning.disabled'].format(bold('pacman')))
|
||||||
|
|
||||||
|
if not self._is_wget_available():
|
||||||
|
warnings.append(self.i18n['arch.warning.disabled'].format(bold('wget')))
|
||||||
|
|
||||||
|
if not git.is_enabled():
|
||||||
|
warnings.append(self.i18n['arch.warning.git'].format(bold('git')))
|
||||||
|
|
||||||
|
return warnings
|
||||||
|
|
||||||
|
def list_suggestions(self, limit: int) -> List[PackageSuggestion]:
|
||||||
|
res = []
|
||||||
|
|
||||||
|
sugs = [(i, p) for i, p in suggestions.ALL.items()]
|
||||||
|
sugs.sort(key=lambda t: t[1].value, reverse=True)
|
||||||
|
|
||||||
|
if limit > 0:
|
||||||
|
sugs = sugs[0:limit]
|
||||||
|
|
||||||
|
sug_names = {s[0] for s in sugs}
|
||||||
|
|
||||||
|
api_res = self.aur_client.get_info(sug_names)
|
||||||
|
|
||||||
|
if api_res:
|
||||||
|
for pkg in api_res:
|
||||||
|
if pkg.get('Name') in sug_names:
|
||||||
|
res.append(PackageSuggestion(self.mapper.map_api_data(pkg, {}), suggestions.ALL.get(pkg['Name'])))
|
||||||
|
|
||||||
|
return res
|
||||||
179
bauh/gems/arch/disk.py
Normal file
179
bauh/gems/arch/disk.py
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Set, List
|
||||||
|
|
||||||
|
from bauh.gems.arch import pacman
|
||||||
|
from bauh.gems.arch.model import ArchPackage
|
||||||
|
|
||||||
|
RE_DESKTOP_ENTRY = re.compile(r'(Exec|Icon)\s*=\s*(.+)')
|
||||||
|
RE_CLEAN_NAME = re.compile(r'^(\w+)-?|_?.+')
|
||||||
|
|
||||||
|
|
||||||
|
def write(app: ArchPackage):
|
||||||
|
Path(app.get_disk_cache_path()).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
with open(app.get_disk_data_path(), 'w+') as f:
|
||||||
|
f.write(json.dumps(app.get_data_to_cache()))
|
||||||
|
|
||||||
|
|
||||||
|
def fill_icon_path(app: ArchPackage, icon_paths: List[str], only_exact_match: bool):
|
||||||
|
ends_with = re.compile(r'.+/{}\.(png|svg)$'.format(app.name), re.IGNORECASE)
|
||||||
|
|
||||||
|
for path in icon_paths:
|
||||||
|
if ends_with.match(path):
|
||||||
|
app.icon_path = path
|
||||||
|
return
|
||||||
|
|
||||||
|
if not only_exact_match:
|
||||||
|
pkg_icons_path = pacman.list_icon_paths({app.name})
|
||||||
|
|
||||||
|
if pkg_icons_path:
|
||||||
|
app.icon_path = pkg_icons_path[0]
|
||||||
|
|
||||||
|
|
||||||
|
def set_icon_path(app: ArchPackage, icon_name: str = None):
|
||||||
|
installed_icons = pacman.list_icon_paths(app.name)
|
||||||
|
|
||||||
|
if installed_icons:
|
||||||
|
exact_match = re.compile(r'.+/{}\..+$'.format(icon_name.split('.')[0] if icon_name else app.name))
|
||||||
|
for icon_path in installed_icons:
|
||||||
|
if exact_match.match(icon_path):
|
||||||
|
app.icon_path = icon_path
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def save(pkgname: str, mirror: str):
|
||||||
|
app = ArchPackage(name=pkgname, mirror=mirror)
|
||||||
|
desktop_files = pacman.list_desktop_entries({pkgname})
|
||||||
|
|
||||||
|
if desktop_files:
|
||||||
|
if len(desktop_files) > 1:
|
||||||
|
exact_match = [f for f in desktop_files if f.endswith('{}.desktop'.format(pkgname))]
|
||||||
|
to_parse = exact_match[0] if exact_match else desktop_files[0]
|
||||||
|
else:
|
||||||
|
to_parse = desktop_files[0]
|
||||||
|
|
||||||
|
app.desktop_entry = to_parse
|
||||||
|
|
||||||
|
with open(to_parse) as f:
|
||||||
|
desktop_entry = f.read()
|
||||||
|
|
||||||
|
for field in RE_DESKTOP_ENTRY.findall(desktop_entry):
|
||||||
|
if field[0] == 'Exec':
|
||||||
|
app.command = field[1].strip()
|
||||||
|
elif field[0] == 'Icon':
|
||||||
|
app.icon_path = field[1].strip()
|
||||||
|
|
||||||
|
if app.icon_path and '/' not in app.icon_path: # if the icon full path is not defined
|
||||||
|
set_icon_path(app, app.icon_path)
|
||||||
|
else:
|
||||||
|
bin_paths = pacman.list_bin_paths({pkgname})
|
||||||
|
|
||||||
|
if bin_paths:
|
||||||
|
if len(bin_paths) > 1:
|
||||||
|
exact_match = [p for p in bin_paths if p.endswith('/' + pkgname)]
|
||||||
|
app.command = exact_match[0] if exact_match else bin_paths[0]
|
||||||
|
else:
|
||||||
|
app.command = bin_paths[0]
|
||||||
|
|
||||||
|
set_icon_path(app)
|
||||||
|
|
||||||
|
Path(app.get_disk_cache_path()).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
with open(app.get_disk_data_path(), 'w+') as f:
|
||||||
|
f.write(json.dumps(app.get_data_to_cache()))
|
||||||
|
|
||||||
|
|
||||||
|
def save_several(pkgnames: Set[str], mirror: str, overwrite: bool = True) -> int:
|
||||||
|
to_cache = {n for n in pkgnames if overwrite or not os.path.exists(ArchPackage.disk_cache_path(n, mirror))}
|
||||||
|
desktop_files = pacman.list_desktop_entries(to_cache)
|
||||||
|
|
||||||
|
no_desktop_files = {}
|
||||||
|
|
||||||
|
to_write = []
|
||||||
|
if desktop_files:
|
||||||
|
desktop_matches, no_exact_match = {}, set()
|
||||||
|
for pkg in to_cache: # first try to find exact matches
|
||||||
|
ends_with = re.compile('.+/{}.desktop$'.format(pkg), re.IGNORECASE)
|
||||||
|
|
||||||
|
for f in desktop_files:
|
||||||
|
if ends_with.match(f):
|
||||||
|
desktop_matches[pkg] = f
|
||||||
|
break
|
||||||
|
|
||||||
|
if pkg not in desktop_matches:
|
||||||
|
no_exact_match.add(pkg)
|
||||||
|
|
||||||
|
if no_exact_match: # check every not matched app individually
|
||||||
|
for pkg in no_exact_match:
|
||||||
|
entries = pacman.list_desktop_entries({pkg})
|
||||||
|
|
||||||
|
if entries:
|
||||||
|
desktop_matches[pkg] = entries[0]
|
||||||
|
|
||||||
|
if not desktop_matches:
|
||||||
|
no_desktop_files = to_cache
|
||||||
|
else:
|
||||||
|
if len(desktop_matches) != len(to_cache):
|
||||||
|
no_desktop_files = {p for p in to_cache if p not in desktop_matches}
|
||||||
|
|
||||||
|
pkgs, apps_icons_noabspath = [], []
|
||||||
|
|
||||||
|
for pkgname, file in desktop_matches.items():
|
||||||
|
p = ArchPackage(name=pkgname, mirror=mirror)
|
||||||
|
p.desktop_entry = file
|
||||||
|
|
||||||
|
with open(file) as f:
|
||||||
|
desktop_entry = f.read()
|
||||||
|
|
||||||
|
for field in RE_DESKTOP_ENTRY.findall(desktop_entry):
|
||||||
|
if field[0] == 'Exec':
|
||||||
|
p.command = field[1].strip()
|
||||||
|
elif field[0] == 'Icon':
|
||||||
|
p.icon_path = field[1].strip()
|
||||||
|
|
||||||
|
if p.icon_path and '/' not in p.icon_path: # if the icon full path is not defined
|
||||||
|
apps_icons_noabspath.append(p)
|
||||||
|
|
||||||
|
pkgs.append(p)
|
||||||
|
|
||||||
|
if apps_icons_noabspath:
|
||||||
|
icon_paths = pacman.list_icon_paths({app.name for app in apps_icons_noabspath})
|
||||||
|
|
||||||
|
if icon_paths:
|
||||||
|
for p in apps_icons_noabspath:
|
||||||
|
fill_icon_path(p, icon_paths, False)
|
||||||
|
|
||||||
|
for p in pkgs:
|
||||||
|
to_write.append(p)
|
||||||
|
|
||||||
|
if no_desktop_files:
|
||||||
|
pkgs = {ArchPackage(name=n, mirror=mirror) for n in no_desktop_files}
|
||||||
|
bin_paths = pacman.list_bin_paths(no_desktop_files)
|
||||||
|
|
||||||
|
if bin_paths:
|
||||||
|
for p in pkgs:
|
||||||
|
ends_with = re.compile(r'.+/{}$'.format(p.name), re.IGNORECASE)
|
||||||
|
|
||||||
|
for path in bin_paths:
|
||||||
|
if ends_with.match(path):
|
||||||
|
p.command = path
|
||||||
|
break
|
||||||
|
|
||||||
|
icon_paths = pacman.list_icon_paths(no_desktop_files)
|
||||||
|
|
||||||
|
if icon_paths:
|
||||||
|
for p in pkgs:
|
||||||
|
fill_icon_path(p, icon_paths, only_exact_match=True)
|
||||||
|
|
||||||
|
for p in pkgs:
|
||||||
|
to_write.append(p)
|
||||||
|
|
||||||
|
if to_write:
|
||||||
|
for p in to_write:
|
||||||
|
write(p)
|
||||||
|
return len(to_write)
|
||||||
|
return 0
|
||||||
|
|
||||||
29
bauh/gems/arch/git.py
Normal file
29
bauh/gems/arch/git.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from bauh.commons.system import new_subprocess
|
||||||
|
|
||||||
|
|
||||||
|
def is_enabled() -> bool:
|
||||||
|
try:
|
||||||
|
new_subprocess(['git', '--version'])
|
||||||
|
return True
|
||||||
|
except FileNotFoundError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def list_commits(proj_dir:str) -> List[dict]:
|
||||||
|
logs = new_subprocess(['git', 'log', '--date=iso'], cwd=proj_dir).stdout
|
||||||
|
|
||||||
|
commits, commit = [], {}
|
||||||
|
for out in new_subprocess(['grep', '-E', 'commit|Date:'], stdin=logs).stdout:
|
||||||
|
if out:
|
||||||
|
line = out.decode()
|
||||||
|
if line.startswith('commit'):
|
||||||
|
commit['commit'] = line.split(' ')[1].strip()
|
||||||
|
elif line.startswith('Date'):
|
||||||
|
commit['date'] = datetime.fromisoformat(line.split(':')[1].strip())
|
||||||
|
commits.append(commit)
|
||||||
|
commit = {}
|
||||||
|
|
||||||
|
return commits
|
||||||
28
bauh/gems/arch/makepkg.py
Normal file
28
bauh/gems/arch/makepkg.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import re
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from bauh.api.abstract.handler import ProcessWatcher
|
||||||
|
from bauh.commons.system import new_subprocess
|
||||||
|
|
||||||
|
RE_DEPS_PATTERN = re.compile(r'\n?\s+->\s(.+)\n')
|
||||||
|
|
||||||
|
|
||||||
|
def check_missing_deps(pkgdir: str, watcher: ProcessWatcher) -> List[str]:
|
||||||
|
depcheck = new_subprocess(['makepkg', '--check'], cwd=pkgdir)
|
||||||
|
|
||||||
|
for o in depcheck.stdout:
|
||||||
|
if o:
|
||||||
|
watcher.print(o.decode())
|
||||||
|
|
||||||
|
error_lines = []
|
||||||
|
for s in depcheck.stderr:
|
||||||
|
if s:
|
||||||
|
line = s.decode()
|
||||||
|
print(line)
|
||||||
|
error_lines.append(line)
|
||||||
|
|
||||||
|
if error_lines:
|
||||||
|
error_str = ''.join(error_lines)
|
||||||
|
|
||||||
|
if 'Missing dependencies' in error_str:
|
||||||
|
return RE_DEPS_PATTERN.findall(error_str)
|
||||||
55
bauh/gems/arch/mapper.py
Normal file
55
bauh/gems/arch/mapper.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from bauh.api.http import HttpClient
|
||||||
|
from bauh.gems.arch.model import ArchPackage
|
||||||
|
|
||||||
|
URL_PKG_DOWNLOAD = 'https://aur.archlinux.org/{}'
|
||||||
|
|
||||||
|
|
||||||
|
class ArchDataMapper:
|
||||||
|
|
||||||
|
def __init__(self, http_client: HttpClient):
|
||||||
|
self.http_client = http_client
|
||||||
|
|
||||||
|
def fill_api_data(self, pkg: ArchPackage, package: dict, fill_version: bool = True):
|
||||||
|
|
||||||
|
version = package.get('Version')
|
||||||
|
|
||||||
|
if version:
|
||||||
|
version = version.split(':')
|
||||||
|
version = version[0] if len(version) == 1 else version[1]
|
||||||
|
|
||||||
|
pkg.id = package.get('ID')
|
||||||
|
pkg.name = package.get('Name')
|
||||||
|
|
||||||
|
if fill_version:
|
||||||
|
pkg.version = version
|
||||||
|
|
||||||
|
pkg.latest_version = version
|
||||||
|
pkg.description = package.get('Description')
|
||||||
|
|
||||||
|
pkg.package_base = package.get('PackageBase')
|
||||||
|
pkg.popularity = package.get('Popularity')
|
||||||
|
pkg.votes = package.get('NumVotes')
|
||||||
|
pkg.maintainer = package.get('Maintainer')
|
||||||
|
pkg.url_download = URL_PKG_DOWNLOAD.format(package['URLPath']) if package.get('URLPath') else None
|
||||||
|
pkg.first_submitted = datetime.fromtimestamp(package['FirstSubmitted']) if package.get('FirstSubmitted') else None
|
||||||
|
pkg.last_modified = datetime.fromtimestamp(package['LastModified']) if package.get('LastModified') else None
|
||||||
|
pkg.update = pkg.version and pkg.latest_version and pkg.latest_version > pkg.version
|
||||||
|
|
||||||
|
def fill_package_build(self, pkg: ArchPackage):
|
||||||
|
res = self.http_client.get(pkg.get_pkg_build_url())
|
||||||
|
|
||||||
|
if res and res.status_code == 200 and res.text:
|
||||||
|
pkg.pkgbuild = res.text
|
||||||
|
|
||||||
|
def map_api_data(self, apidata: dict, installed: dict) -> ArchPackage:
|
||||||
|
data = installed.get(apidata.get('Name'))
|
||||||
|
app = ArchPackage(name=apidata.get('Name'), installed=bool(data), mirror='aur')
|
||||||
|
|
||||||
|
if data:
|
||||||
|
app.version = data.get('version')
|
||||||
|
app.description = data.get('description')
|
||||||
|
|
||||||
|
self.fill_api_data(app, apidata, fill_version=not data)
|
||||||
|
return app
|
||||||
20
bauh/gems/arch/message.py
Normal file
20
bauh/gems/arch/message.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
from bauh.api.abstract.handler import ProcessWatcher
|
||||||
|
from bauh.api.abstract.view import MessageType
|
||||||
|
|
||||||
|
|
||||||
|
def show_dep_not_installed(watcher: ProcessWatcher, pkgname: str, depname: str, i18n: dict):
|
||||||
|
watcher.show_message(title=i18n['error'],
|
||||||
|
body=i18n['arch.install.dependency.install.error'].format('"{}"'.format(depname), '"{}"'.format(pkgname)),
|
||||||
|
type_=MessageType.ERROR)
|
||||||
|
|
||||||
|
|
||||||
|
def show_dep_not_found(depname: str, i18n: dict, watcher: ProcessWatcher):
|
||||||
|
watcher.show_message(title=i18n['arch.install.dep_not_found.title'],
|
||||||
|
body=i18n['arch.install.dep_not_found.body'].format('"{}"'.format(depname)),
|
||||||
|
type_=MessageType.ERROR)
|
||||||
|
|
||||||
|
|
||||||
|
def show_optdep_not_installed(depname: str, watcher: ProcessWatcher, i18n: dict):
|
||||||
|
watcher.show_message(title=i18n['error'],
|
||||||
|
body=i18n['arch.install.optdep.error'].format('"{}"'.format(depname)),
|
||||||
|
type_=MessageType.ERROR)
|
||||||
109
bauh/gems/arch/model.py
Normal file
109
bauh/gems/arch/model.py
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import datetime
|
||||||
|
|
||||||
|
from bauh.api.abstract.model import SoftwarePackage
|
||||||
|
from bauh.api.constants import CACHE_PATH
|
||||||
|
from bauh.commons import resource
|
||||||
|
|
||||||
|
from bauh.gems.arch import ROOT_DIR
|
||||||
|
|
||||||
|
|
||||||
|
class ArchPackage(SoftwarePackage):
|
||||||
|
|
||||||
|
def __init__(self, name: str = None, version: str = None, latest_version: str = None, description: str = None,
|
||||||
|
package_base: str = None, votes: int = None, popularity: float = None,
|
||||||
|
first_submitted: datetime.datetime = None, last_modified: datetime.datetime = None,
|
||||||
|
maintainer: str = None, url_download: str = None, pkgbuild: str = None, mirror: str = None,
|
||||||
|
desktop_entry: str = None, installed: bool = False):
|
||||||
|
|
||||||
|
super(ArchPackage, self).__init__(name=name, version=version, latest_version=latest_version, description=description, installed=installed)
|
||||||
|
self.package_base = package_base
|
||||||
|
self.votes = votes
|
||||||
|
self.popularity = popularity
|
||||||
|
self.maintainer = maintainer
|
||||||
|
self.url_download = url_download
|
||||||
|
self.first_submitted = first_submitted
|
||||||
|
self.last_modified = last_modified
|
||||||
|
self.pkgbuild = pkgbuild
|
||||||
|
self.mirror = mirror
|
||||||
|
self.command = None
|
||||||
|
self.icon_path = None
|
||||||
|
self.downgrade_enabled = False
|
||||||
|
self.desktop_entry = desktop_entry
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def disk_cache_path(pkgname: str, mirror: str):
|
||||||
|
return CACHE_PATH + '/arch/installed/' + ('aur' if mirror == 'aur' else 'mirror') + '/' + pkgname
|
||||||
|
|
||||||
|
def get_pkg_build_url(self):
|
||||||
|
if self.package_base:
|
||||||
|
return 'https://aur.archlinux.org/cgit/aur.git/plain/PKGBUILD?h=' + self.package_base
|
||||||
|
|
||||||
|
def has_history(self):
|
||||||
|
return self.installed
|
||||||
|
|
||||||
|
def has_info(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def can_be_installed(self):
|
||||||
|
return super(ArchPackage, self).can_be_installed() and self.url_download
|
||||||
|
|
||||||
|
def can_be_downgraded(self):
|
||||||
|
return self.installed and self.downgrade_enabled
|
||||||
|
|
||||||
|
def get_type(self):
|
||||||
|
return 'aur' if self.mirror == 'aur' else 'arch'
|
||||||
|
|
||||||
|
def get_default_icon_path(self) -> str:
|
||||||
|
return self.get_type_icon_path()
|
||||||
|
|
||||||
|
def get_disk_icon_path(self) -> str:
|
||||||
|
return self.icon_path
|
||||||
|
|
||||||
|
def get_type_icon_path(self):
|
||||||
|
return resource.get_path('img/arch.png', ROOT_DIR) # TODO change icon when from mirrors
|
||||||
|
|
||||||
|
def is_application(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def supports_disk_cache(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_disk_cache_path(self) -> str:
|
||||||
|
if self.name:
|
||||||
|
return self.disk_cache_path(self.name, self.mirror)
|
||||||
|
|
||||||
|
def get_data_to_cache(self) -> dict:
|
||||||
|
cache = {}
|
||||||
|
|
||||||
|
# required attrs to cache
|
||||||
|
for a in {'command', 'icon_path', 'mirror'}:
|
||||||
|
val = getattr(self, a)
|
||||||
|
|
||||||
|
if val:
|
||||||
|
cache[a] = val
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if self.desktop_entry:
|
||||||
|
cache['desktop_entry'] = self.desktop_entry
|
||||||
|
|
||||||
|
return cache
|
||||||
|
|
||||||
|
def fill_cached_data(self, data: dict):
|
||||||
|
for a in {'command', 'icon_path', 'mirror', 'desktop_entry'}:
|
||||||
|
val = data.get(a)
|
||||||
|
if val:
|
||||||
|
setattr(self, a, val)
|
||||||
|
|
||||||
|
if a == 'icon_path':
|
||||||
|
self.icon_url = val
|
||||||
|
|
||||||
|
def can_be_run(self) -> bool:
|
||||||
|
# only returns if there is a desktop entry set for the application to avoid running command-line applications
|
||||||
|
return bool(self.desktop_entry)
|
||||||
|
|
||||||
|
def get_command(self) -> str:
|
||||||
|
return self.command
|
||||||
|
|
||||||
|
def get_publisher(self):
|
||||||
|
return self.maintainer
|
||||||
131
bauh/gems/arch/pacman.py
Normal file
131
bauh/gems/arch/pacman.py
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import re
|
||||||
|
from typing import List, Set
|
||||||
|
|
||||||
|
from bauh.commons.system import run_cmd, new_subprocess, new_root_subprocess, SystemProcess
|
||||||
|
|
||||||
|
|
||||||
|
def is_enabled() -> bool:
|
||||||
|
try:
|
||||||
|
new_subprocess(['pacman', '--version'])
|
||||||
|
return True
|
||||||
|
except FileNotFoundError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_mirrors(pkgs: Set[str]) -> dict:
|
||||||
|
pkgre = '|'.join(pkgs)
|
||||||
|
|
||||||
|
searchres = new_subprocess(['pacman', '-Ss', pkgre]).stdout
|
||||||
|
mirrors = {}
|
||||||
|
|
||||||
|
for line in new_subprocess(['grep', '-E', '.+/({}) '.format(pkgre)], stdin=searchres).stdout:
|
||||||
|
if line:
|
||||||
|
match = line.decode()
|
||||||
|
for p in pkgs:
|
||||||
|
if p in match:
|
||||||
|
mirrors[p] = match.split('/')[0]
|
||||||
|
|
||||||
|
return mirrors
|
||||||
|
|
||||||
|
|
||||||
|
def is_available_from_mirrors(pkg_name: str) -> bool:
|
||||||
|
return bool(run_cmd('pacman -Ss ' + pkg_name))
|
||||||
|
|
||||||
|
|
||||||
|
def get_info(pkg_name) -> str:
|
||||||
|
return run_cmd('pacman -Qi ' + pkg_name)
|
||||||
|
|
||||||
|
|
||||||
|
def get_info_list(pkg_name: str) -> List[tuple]:
|
||||||
|
info = get_info(pkg_name)
|
||||||
|
if info:
|
||||||
|
return re.findall(r'(\w+\s?\w+)\s+:\s+(.+(\n\s+.+)*)', info)
|
||||||
|
|
||||||
|
|
||||||
|
def get_info_dict(pkg_name: str) -> dict:
|
||||||
|
info_list = get_info_list(pkg_name)
|
||||||
|
|
||||||
|
if info_list:
|
||||||
|
info_dict = {}
|
||||||
|
for info_data in info_list:
|
||||||
|
attr = info_data[0].lower()
|
||||||
|
info_dict[attr] = info_data[1] if '\n' not in info_data[1] else ' '.join([l.strip() for l in info_data[1].split('\n')])
|
||||||
|
|
||||||
|
if info_dict[attr] == 'None':
|
||||||
|
info_dict[attr] = None
|
||||||
|
|
||||||
|
return info_dict
|
||||||
|
|
||||||
|
|
||||||
|
def list_installed() -> Set[str]:
|
||||||
|
return {out.decode().strip() for out in new_subprocess(['pacman', '-Qq']).stdout if out}
|
||||||
|
|
||||||
|
|
||||||
|
def list_and_map_installed() -> dict: # returns a dict with with package names as keys and versions as values
|
||||||
|
installed = new_subprocess(['pacman', '-Qq']).stdout # retrieving all installed package names
|
||||||
|
allinfo = new_subprocess(['pacman', '-Qi'], stdin=installed).stdout # retrieving all installed packages info
|
||||||
|
|
||||||
|
pkgs, current_pkg = {'mirrors': {}, 'not_signed': {}}, {}
|
||||||
|
for out in new_subprocess(['grep', '-E', '(Name|Description|Version|Validated By)'], stdin=allinfo).stdout: # filtering only the Name and Validated By fields:
|
||||||
|
if out:
|
||||||
|
line = out.decode()
|
||||||
|
|
||||||
|
if line.startswith('Name'):
|
||||||
|
current_pkg['name'] = line.split(':')[1].strip()
|
||||||
|
elif line.startswith('Version'):
|
||||||
|
version = line.split(':')
|
||||||
|
current_pkg['version'] = version[len(version) - 1].strip()
|
||||||
|
elif line.startswith('Description'):
|
||||||
|
current_pkg['description'] = line.split(':')[1].strip()
|
||||||
|
elif line.startswith('Validated'):
|
||||||
|
|
||||||
|
if line.split(':')[1].strip().lower() == 'none':
|
||||||
|
pkgs['not_signed'][current_pkg['name']] = {'version': current_pkg['version'], 'description': current_pkg['description']}
|
||||||
|
|
||||||
|
current_pkg = {}
|
||||||
|
|
||||||
|
return pkgs
|
||||||
|
|
||||||
|
|
||||||
|
def install_as_process(pkgpath: str, root_password: str, aur: bool, pkgdir: str = '.') -> SystemProcess:
|
||||||
|
|
||||||
|
if aur:
|
||||||
|
cmd = ['pacman', '-U', pkgpath, '--noconfirm'] # pkgpath = install file path
|
||||||
|
else:
|
||||||
|
cmd = ['pacman', '-S', pkgpath, '--noconfirm'] # pkgpath = pkgname
|
||||||
|
|
||||||
|
return SystemProcess(new_root_subprocess(cmd, root_password, cwd=pkgdir))
|
||||||
|
|
||||||
|
|
||||||
|
def list_desktop_entries(pkgnames: Set[str]) -> List[str]:
|
||||||
|
if pkgnames:
|
||||||
|
installed_files = new_subprocess(['pacman', '-Qlq', *pkgnames]).stdout
|
||||||
|
|
||||||
|
desktop_files = []
|
||||||
|
for out in new_subprocess(['grep', '-E', '.desktop'], stdin=installed_files).stdout:
|
||||||
|
if out:
|
||||||
|
desktop_files.append(out.decode().strip())
|
||||||
|
|
||||||
|
return desktop_files
|
||||||
|
|
||||||
|
|
||||||
|
def list_icon_paths(pkgnames: Set[str]) -> List[str]:
|
||||||
|
installed_files = new_subprocess(['pacman', '-Qlq', *pkgnames]).stdout
|
||||||
|
|
||||||
|
icon_files = []
|
||||||
|
for out in new_subprocess(['grep', '-E', '.(png|svg|jpeg)'], stdin=installed_files).stdout:
|
||||||
|
if out:
|
||||||
|
icon_files.append(out.decode().strip())
|
||||||
|
|
||||||
|
return icon_files
|
||||||
|
|
||||||
|
|
||||||
|
def list_bin_paths(pkgnames: Set[str]) -> List[str]:
|
||||||
|
installed_files = new_subprocess(['pacman', '-Qlq', *pkgnames]).stdout
|
||||||
|
|
||||||
|
bin_paths = []
|
||||||
|
for out in new_subprocess(['grep', '-E', '^/usr/bin/.+'], stdin=installed_files).stdout:
|
||||||
|
if out:
|
||||||
|
bin_paths.append(out.decode().strip())
|
||||||
|
|
||||||
|
return bin_paths
|
||||||
13
bauh/gems/arch/pkgbuild.py
Normal file
13
bauh/gems/arch/pkgbuild.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import re
|
||||||
|
from typing import Set
|
||||||
|
|
||||||
|
RE_PKGBUILD_OPTDEPS = re.compile(r"optdepends = ([^:\s]+)")
|
||||||
|
RE_PKGBUILD_DEPSON = re.compile(r"depends = ([^:\s]+)")
|
||||||
|
|
||||||
|
|
||||||
|
def read_optdeps(srcinfo: str) -> Set[str]:
|
||||||
|
return set(RE_PKGBUILD_OPTDEPS.findall(srcinfo))
|
||||||
|
|
||||||
|
|
||||||
|
def read_depends_on(srcinfo: str) -> Set[str]:
|
||||||
|
return set(RE_PKGBUILD_DEPSON.findall(srcinfo))
|
||||||
BIN
bauh/gems/arch/resources/img/arch.png
Executable file
BIN
bauh/gems/arch/resources/img/arch.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 905 B |
49
bauh/gems/arch/resources/locale/en
Normal file
49
bauh/gems/arch/resources/locale/en
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
gem.arch.label=Arch ( AUR )
|
||||||
|
arch.install.conflict.popup.title=Conflict detected
|
||||||
|
arch.install.conflict.popup.body=The applications {} are in conflict. You must uninstall one to install the other. Continue ?
|
||||||
|
arch.missing_deps.title=Missing dependencies
|
||||||
|
arch.missing_deps.body=The following dependencies must be installed before the {} installation continues
|
||||||
|
arch.downgrade.error=Error
|
||||||
|
arch.downgrade.impossible=It is not possible to downgrade {}
|
||||||
|
aur.history.1_version=version
|
||||||
|
aur.history.2_release=release
|
||||||
|
aur.history.3_date=date
|
||||||
|
arch.downloading.package=Downloading the package
|
||||||
|
arch.uncompressing.package=Uncompressing the package
|
||||||
|
arch.checking.deps=Checking {} dependencies
|
||||||
|
arch.missing_deps_found=Missing dependencies for {}
|
||||||
|
arch.building.package=Building package {}
|
||||||
|
arch.checking.conflicts=Checking any conflicts with {}
|
||||||
|
arch.installing.package=Installing {} package
|
||||||
|
arch.uninstalling.conflict=Uninstalling conflicting package {}
|
||||||
|
arch.uninstalling.conflict.fail=It was not possible to uninstall the conflicting package {}
|
||||||
|
arch.clone=Cloning the AUR repository {}
|
||||||
|
arch.downgrade.reading_commits=Reading the repository commits
|
||||||
|
arch.downgrade.version_found=Current package version found
|
||||||
|
arch.downgrade.install_older=Installing older version
|
||||||
|
aur.info.01_id=id
|
||||||
|
aur.info.02_name=name
|
||||||
|
aur.info.03_version=version
|
||||||
|
aur.info.04_popularity=popularity
|
||||||
|
aur.info.05_votes=votes
|
||||||
|
aur.info.06_package_base=package base
|
||||||
|
aur.info.07_maintainer=maintainer
|
||||||
|
aur.info.08_first_submitted=first submitted
|
||||||
|
aur.info.09_last_modified=app.last_modified
|
||||||
|
aur.info.10_url=url download
|
||||||
|
aur.info.11_pkg_build_url=url pkgbuild
|
||||||
|
aur.info.13_pkg_build=pkgbuild
|
||||||
|
aur.info.11_dependson=dependencies
|
||||||
|
aur.info.12_optdepends=optional dependencies
|
||||||
|
arch.install.dep_not_found.title=Dependency not found
|
||||||
|
arch.install.dep_not_found.body=Required dependency {} was not found in AUR nor in default mirrors. Installation cancelled.
|
||||||
|
arch.install.dependency.install=Installing package dependency {}
|
||||||
|
arch.install.dependency.install.error=Could not install dependent package {}. Installation of {} aborted.
|
||||||
|
arch.uninstall.required_by={} cannot be uninstalled because it is necessary for these following packages to work
|
||||||
|
arch.uninstall.required_by.advice=Uninstall them first before uninstalling {}.
|
||||||
|
arch.install.optdeps.request.title=Optional dependencies
|
||||||
|
arch.install.optdeps.request.body={} was succesfully installed ! There are some optional associated packages that you might want to install as well (check those you want)
|
||||||
|
arch.install.optdep.error=Could not install the optional package {}
|
||||||
|
arch.optdeps.checking=Checking {} optional dependencies
|
||||||
|
arch.warning.disabled={} seems not to be installed. It will not be possible to manage Arch / AUR packages.
|
||||||
|
arch.warning.git={} seems not to be installed. It will not be possible to downgrade AUR packages.
|
||||||
81
bauh/gems/arch/resources/locale/es
Normal file
81
bauh/gems/arch/resources/locale/es
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
gem.arch.label=Arch ( AUR )
|
||||||
|
aur.info.architecture=arquitectura
|
||||||
|
aur.info.architecture.any=cualquier
|
||||||
|
aur.info.build date=fecha de construcción
|
||||||
|
aur.info.depends on=depende
|
||||||
|
aur.info.description=descripción
|
||||||
|
aur.info.install date=fecha de instalación
|
||||||
|
aur.info.install reason=razón da instalación
|
||||||
|
aur.info.install reason.explicitly installed=instalado explícitamente
|
||||||
|
aur.info.install script=script de instalación
|
||||||
|
aur.info.install script.no=ninguno
|
||||||
|
aur.info.installed size=tamaño da instalación
|
||||||
|
aur.info.licenses=licencias
|
||||||
|
aur.info.name=nombre
|
||||||
|
aur.info.optional deps=dependencias opcionales
|
||||||
|
aur.info.packager=empaquetador
|
||||||
|
aur.info.packager.unknown packager=desconocido
|
||||||
|
aur.info.url=url
|
||||||
|
aur.info.version=versión
|
||||||
|
aur.info.arch=arquitectura
|
||||||
|
aur.info.arch.any=cualquier
|
||||||
|
aur.info.depends=depende
|
||||||
|
aur.info.pkgdesc=descripción
|
||||||
|
aur.info.pkgname=nombre
|
||||||
|
aur.info.pkgrel=lanzamiento
|
||||||
|
aur.info.pkgver=versión
|
||||||
|
aur.info.source=origen
|
||||||
|
aur.info.optdepends=dependencias opcionales
|
||||||
|
aur.info.license=licencia
|
||||||
|
aur.info.validpgpkeys=llaves PGP válidas
|
||||||
|
aur.info.options=opciones
|
||||||
|
aur.info.provides=provee
|
||||||
|
aur.info.conflicts with=conflicta
|
||||||
|
arch.install.conflict.popup.title=Conflicto detectado
|
||||||
|
arch.install.conflict.popup.body=Los aplicativos {} estan en conflicto. Debe desinstalar uno para instalar el otro. ¿Continuar?
|
||||||
|
arch.missing_deps.title=Dependencias faltantes
|
||||||
|
arch.missing_deps.body=Deben instalarse las siguientes dependencias antes de continuar la instalación de {}
|
||||||
|
arch.downgrade.error=Error
|
||||||
|
arch.downgrade.impossible=No es posible revertir la versión de {}
|
||||||
|
aur.history.1_version=versión
|
||||||
|
aur.history.2_release=lanzamiento
|
||||||
|
aur.history.3_date=fecha
|
||||||
|
arch.downloading.package=Descargando el paquete
|
||||||
|
arch.uncompressing.package=Descomprimindo el paquete
|
||||||
|
arch.checking.deps=Verificando las dependencias de {}
|
||||||
|
arch.missing_deps_found=Dependencias faltantes para {}
|
||||||
|
arch.building.package=Construyendo el paquete {}
|
||||||
|
arch.checking.conflicts=Verificando se hay conflictos con {}
|
||||||
|
arch.installing.package=Instalando el paquete {}
|
||||||
|
arch.uninstalling.conflict=Eliminando el paquete conflictivo {}
|
||||||
|
arch.uninstalling.conflict.fail=No fue posible desinstalar el paquete conflictivo {}
|
||||||
|
arch.clone=Clonando el repositorio {} de AUR
|
||||||
|
arch.downgrade.reading_commits=Leyendo los commits del repositorio
|
||||||
|
arch.downgrade.version_found=Version actual del paquete encontrada
|
||||||
|
arch.downgrade.install_older=Instalando versión anterior
|
||||||
|
aur.info.01_id=id
|
||||||
|
aur.info.02_name=nombre
|
||||||
|
aur.info.03_version=versión
|
||||||
|
aur.info.04_popularity=popularidad
|
||||||
|
aur.info.05_votes=votos
|
||||||
|
aur.info.06_package_base=paquete base
|
||||||
|
aur.info.07_maintainer=mantenedor
|
||||||
|
aur.info.08_first_submitted=primero envio
|
||||||
|
aur.info.09_last_modified=última modificación
|
||||||
|
aur.info.10_url=url download
|
||||||
|
aur.info.11_pkg_build_url=url pkgbuild
|
||||||
|
aur.info.13_pkg_build=pkgbuild
|
||||||
|
aur.info.11_dependson=dependencias
|
||||||
|
aur.info.12_optdepends=dependencias opcionales
|
||||||
|
arch.install.dep_not_found.title=Dependencia no encontrada
|
||||||
|
arch.install.dep_not_found.body=No se encontró la dependencia requerida {} en AUR ni en los espejos predeterminados. Instalación cancelada.
|
||||||
|
arch.install.dependency.install=Instalando el paquete dependiente {}
|
||||||
|
arch.install.dependency.install.error=No se pudo instalar el paquete dependiente {}. Instalación de {} abortada.
|
||||||
|
arch.uninstall.required_by=No se puede desinstalar {} porque es necesario para que los siguientes paquetes funcionen
|
||||||
|
arch.uninstall.required_by.advice=Debe desinstalarlos primero antes de desinstalar {}
|
||||||
|
arch.install.optdeps.request.title=Dependencias opcionales
|
||||||
|
arch.install.optdeps.request.body=¡{} se instaló correctamente! También hay algunos paquetes opcionales asociados que es posible que desee instalar (marque los que desee)
|
||||||
|
arch.install.optdep.error=No se pudo instalar el paquete opcional {}
|
||||||
|
arch.optdeps.checking=Verificando las dependencias opcionales de {}
|
||||||
|
arch.warning.disabled={} parece no estar instalado. No será posible administrar paquetes Arch / AUR.
|
||||||
|
arch.warning.git={} parece no estar instalado. No será posible revertir las versiones de paquetes Arch / AUR.
|
||||||
81
bauh/gems/arch/resources/locale/pt
Normal file
81
bauh/gems/arch/resources/locale/pt
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
gem.arch.label=Arch ( AUR )
|
||||||
|
aur.info.architecture=arquitetura
|
||||||
|
aur.info.architecture.any=qualquer
|
||||||
|
aur.info.build date=data de construção
|
||||||
|
aur.info.depends on=depende
|
||||||
|
aur.info.description=descrição
|
||||||
|
aur.info.install date=data de instalação
|
||||||
|
aur.info.install reason=razão da instalação
|
||||||
|
aur.info.install reason.explicitly installed=instalado explicitamente
|
||||||
|
aur.info.install script=script de instalação
|
||||||
|
aur.info.install script.no=nenhum
|
||||||
|
aur.info.installed size=tamanho da instalação
|
||||||
|
aur.info.licenses=licenças
|
||||||
|
aur.info.name=nome
|
||||||
|
aur.info.optional deps=dependências opcionais
|
||||||
|
aur.info.packager=empacotador
|
||||||
|
aur.info.packager.unknown packager=desconhecido
|
||||||
|
aur.info.url=url
|
||||||
|
aur.info.version=versão
|
||||||
|
aur.info.arch=arquitetura
|
||||||
|
aur.info.arch.any=qualquer
|
||||||
|
aur.info.depends=depende
|
||||||
|
aur.info.pkgdesc=descrição
|
||||||
|
aur.info.pkgname=nome
|
||||||
|
aur.info.pkgrel=lançamento
|
||||||
|
aur.info.pkgver=versão
|
||||||
|
aur.info.source=origem
|
||||||
|
aur.info.optdepends=dependências opcionais
|
||||||
|
aur.info.license=licença
|
||||||
|
aur.info.validpgpkeys=chaves PGP válidas
|
||||||
|
aur.info.options=opções
|
||||||
|
aur.info.provides=provê
|
||||||
|
aur.info.conflicts with=conflita
|
||||||
|
arch.install.conflict.popup.title=Conflito detectado
|
||||||
|
arch.install.conflict.popup.body=Os aplicativos {} estão em conflito. Você precisa desinstalar um para instalar o outro. Continuar ?
|
||||||
|
arch.missing_deps.title=Dependências ausentes
|
||||||
|
arch.missing_deps.body=As seguintes dependências devem ser instaladas antes de continuar com a instalação de {}
|
||||||
|
arch.downgrade.error=Erro
|
||||||
|
arch.downgrade.impossible=Não é possível reverter a versão de {}
|
||||||
|
aur.history.1_version=versão
|
||||||
|
aur.history.2_release=lançamento
|
||||||
|
aur.history.3_date=data
|
||||||
|
arch.downloading.package=Baixando o pacote
|
||||||
|
arch.uncompressing.package=Descompactando o pacote
|
||||||
|
arch.checking.deps=Checando as dependências de {}
|
||||||
|
arch.missing_deps_found=Dependencias ausentes para {}
|
||||||
|
arch.building.package=Construindo o pacote {}
|
||||||
|
arch.checking.conflicts=Verificando se há conflitos com {}
|
||||||
|
arch.installing.package=Instalando o pacote {}
|
||||||
|
arch.uninstalling.conflict=Desinstalando o pacote conflitante {}
|
||||||
|
arch.uninstalling.conflict.fail=Não foi possível desinstalar o pacote conflitante {}
|
||||||
|
arch.clone=Clonando o repositório {} do AUR
|
||||||
|
arch.downgrade.reading_commits=Lendo os commits do repositório
|
||||||
|
arch.downgrade.version_found=Versão atual do pacote encontrada
|
||||||
|
arch.downgrade.install_older=Instalando versão anterior
|
||||||
|
aur.info.01_id=id
|
||||||
|
aur.info.02_name=nome
|
||||||
|
aur.info.03_version=versão
|
||||||
|
aur.info.04_popularity=popularidade
|
||||||
|
aur.info.05_votes=votos
|
||||||
|
aur.info.06_package_base=pacote base
|
||||||
|
aur.info.07_maintainer=mantenedor
|
||||||
|
aur.info.08_first_submitted=primeira submissão
|
||||||
|
aur.info.09_last_modified=última modificação
|
||||||
|
aur.info.10_url=url download
|
||||||
|
aur.info.11_pkg_build_url=url pkgbuild
|
||||||
|
aur.info.13_pkg_build=pkgbuild
|
||||||
|
aur.info.11_dependson=dependências
|
||||||
|
aur.info.12_optdepends=dependências opcionais
|
||||||
|
arch.install.dep_not_found.title=Dependência não encontrada
|
||||||
|
arch.install.dep_not_found.body=A dependência {} não foi encontrado no AUR nem nos espelhos padrões. Instalação cancelada.
|
||||||
|
arch.install.dependency.install=Instalando o pacote dependente {}
|
||||||
|
arch.install.dependency.install.error=Não foi possível instalar o pacote dependente {}. Instalação de {} abortada.
|
||||||
|
arch.uninstall.required_by={} não pode ser desinstalado porque ele é necessário para o funcionamento dos seguintes pacotes
|
||||||
|
arch.uninstall.required_by.advice=Desinstale eles primeiro antes de desinstalar {}
|
||||||
|
arch.install.optdeps.request.title=Dependências opcionais
|
||||||
|
arch.install.optdeps.request.body={} foi instalado com sucesso ! Existem alguns pacotes opcionais associados que talvez você também queira instalar (marque os desejados)
|
||||||
|
arch.install.optdep.error=Não foi possível instalar o pacote opcional {}
|
||||||
|
arch.optdeps.checking=Verificando as dependências opcionais de {}
|
||||||
|
arch.warning.disabled={} parece não estar instalado. Não será possível gerenciar pacotes Arch / AUR.
|
||||||
|
arch.warning.git={} parece não estar instalado. Não será possível reverter versões de pacotes Arch / AUR.
|
||||||
3
bauh/gems/arch/suggestions.py
Normal file
3
bauh/gems/arch/suggestions.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from bauh.api.abstract.model import SuggestionPriority
|
||||||
|
|
||||||
|
ALL = {'google-chrome': SuggestionPriority.HIGH}
|
||||||
53
bauh/gems/arch/worker.py
Normal file
53
bauh/gems/arch/worker.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from multiprocessing import Process
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
from bauh.api.abstract.context import ApplicationContext
|
||||||
|
from bauh.api.abstract.controller import SoftwareManager
|
||||||
|
|
||||||
|
from bauh.gems.arch import pacman, disk
|
||||||
|
|
||||||
|
URL_INDEX = 'https://aur.archlinux.org/packages.gz'
|
||||||
|
URL_INFO = 'https://aur.archlinux.org/rpc/?v=5&type=info&arg={}'
|
||||||
|
|
||||||
|
|
||||||
|
class AURIndexUpdater(Thread):
|
||||||
|
|
||||||
|
def __init__(self, context: ApplicationContext, man: SoftwareManager):
|
||||||
|
super(AURIndexUpdater, self).__init__(daemon=True)
|
||||||
|
self.http_client = context.http_client
|
||||||
|
self.logger = context.logger
|
||||||
|
self.man = man
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
|
||||||
|
while True:
|
||||||
|
self.logger.info('Pre-indexing AUR packages in memory')
|
||||||
|
res = self.http_client.get(URL_INDEX)
|
||||||
|
|
||||||
|
if res and res.text:
|
||||||
|
self.man.names_index = {n.replace('-', '').replace('_', '').replace('.', ''): n for n in res.text.split('\n') if n and not n.startswith('#')}
|
||||||
|
self.logger.info('Pre-indexed {} AUR package names in memory'.format(len(self.man.names_index)))
|
||||||
|
else:
|
||||||
|
self.logger.warning('No data returned from: {}'.format(URL_INDEX))
|
||||||
|
|
||||||
|
time.sleep(5 * 60) # updates every 5 minutes
|
||||||
|
|
||||||
|
|
||||||
|
class ArchDiskCacheUpdater(Thread if bool(os.getenv('BAUH_DEBUG', 0)) else Process):
|
||||||
|
|
||||||
|
def __init__(self, logger: logging.Logger):
|
||||||
|
super(ArchDiskCacheUpdater, self).__init__(daemon=True)
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.logger.info('Pre-caching installed AUR packages data to disk')
|
||||||
|
installed = pacman.list_and_map_installed()
|
||||||
|
|
||||||
|
saved = 0
|
||||||
|
if installed and installed['not_signed']:
|
||||||
|
saved = disk.save_several({app for app in installed['not_signed']}, 'aur', overwrite=False)
|
||||||
|
|
||||||
|
self.logger.info('Pre-cached data of {} AUR packages to disk'.format(saved))
|
||||||
3
bauh/gems/flatpak/__init__.py
Normal file
3
bauh/gems/flatpak/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import os
|
||||||
|
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
2
bauh/gems/flatpak/constants.py
Normal file
2
bauh/gems/flatpak/constants.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
FLATHUB_URL = 'https://flathub.org'
|
||||||
|
FLATHUB_API_URL = FLATHUB_URL + '/api/v1'
|
||||||
208
bauh/gems/flatpak/controller.py
Normal file
208
bauh/gems/flatpak/controller.py
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import List, Set, Type
|
||||||
|
|
||||||
|
from bauh.api.abstract.controller import SearchResult, SoftwareManager, ApplicationContext
|
||||||
|
from bauh.api.abstract.disk import DiskCacheLoader
|
||||||
|
from bauh.api.abstract.handler import ProcessWatcher
|
||||||
|
from bauh.api.abstract.model import PackageHistory, PackageUpdate, SoftwarePackage, PackageSuggestion
|
||||||
|
from bauh.api.abstract.view import MessageType
|
||||||
|
from bauh.commons.html import strip_html
|
||||||
|
from bauh.commons.system import SystemProcess, ProcessHandler
|
||||||
|
|
||||||
|
from bauh.gems.flatpak import flatpak, suggestions
|
||||||
|
from bauh.gems.flatpak.model import FlatpakApplication
|
||||||
|
from bauh.gems.flatpak.worker import FlatpakAsyncDataLoader, FlatpakUpdateLoader
|
||||||
|
|
||||||
|
|
||||||
|
class FlatpakManager(SoftwareManager):
|
||||||
|
|
||||||
|
def __init__(self, context: ApplicationContext):
|
||||||
|
super(FlatpakManager, self).__init__(context=context)
|
||||||
|
self.i18n = context.i18n
|
||||||
|
self.api_cache = context.cache_factory.new()
|
||||||
|
context.disk_loader_factory.map(FlatpakApplication, self.api_cache)
|
||||||
|
|
||||||
|
def get_managed_types(self) -> Set["type"]:
|
||||||
|
return {FlatpakApplication}
|
||||||
|
|
||||||
|
def _map_to_model(self, app_json: dict, installed: bool, disk_loader: DiskCacheLoader) -> FlatpakApplication:
|
||||||
|
|
||||||
|
app = FlatpakApplication(arch=app_json.get('arch'),
|
||||||
|
branch=app_json.get('branch'),
|
||||||
|
origin=app_json.get('origin'),
|
||||||
|
runtime=app_json.get('runtime'),
|
||||||
|
ref=app_json.get('ref'),
|
||||||
|
commit=app_json.get('commit'),
|
||||||
|
id=app_json.get('id'),
|
||||||
|
name=app_json.get('name'),
|
||||||
|
version=app_json.get('version'),
|
||||||
|
latest_version=app_json.get('latest_version'))
|
||||||
|
|
||||||
|
app.installed = installed
|
||||||
|
|
||||||
|
api_data = self.api_cache.get(app_json['id'])
|
||||||
|
|
||||||
|
expired_data = api_data and api_data.get('expires_at') and api_data['expires_at'] <= datetime.utcnow()
|
||||||
|
|
||||||
|
if not api_data or expired_data:
|
||||||
|
if not app_json['runtime']:
|
||||||
|
if disk_loader:
|
||||||
|
disk_loader.fill(app) # preloading cached disk data
|
||||||
|
FlatpakAsyncDataLoader(app=app, api_cache=self.api_cache, manager=self, context=self.context).start()
|
||||||
|
|
||||||
|
else:
|
||||||
|
app.fill_cached_data(api_data)
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
def search(self, words: str, disk_loader: DiskCacheLoader, limit: int = -1) -> SearchResult:
|
||||||
|
|
||||||
|
res = SearchResult([], [], 0)
|
||||||
|
apps_found = flatpak.search(words)
|
||||||
|
|
||||||
|
if apps_found:
|
||||||
|
already_read = set()
|
||||||
|
installed_apps = self.read_installed(disk_loader=disk_loader).installed
|
||||||
|
|
||||||
|
if installed_apps:
|
||||||
|
for app_found in apps_found:
|
||||||
|
for installed_app in installed_apps:
|
||||||
|
if app_found['id'] == installed_app.id:
|
||||||
|
res.installed.append(installed_app)
|
||||||
|
already_read.add(app_found['id'])
|
||||||
|
|
||||||
|
if len(apps_found) > len(already_read):
|
||||||
|
for app_found in apps_found:
|
||||||
|
if app_found['id'] not in already_read:
|
||||||
|
res.new.append(self._map_to_model(app_found, False, disk_loader))
|
||||||
|
|
||||||
|
res.total = len(res.installed) + len(res.new)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def read_installed(self, disk_loader: DiskCacheLoader, limit: int = -1, only_apps: bool = False, pkg_types: Set[Type[SoftwarePackage]] = None) -> SearchResult:
|
||||||
|
installed = flatpak.list_installed()
|
||||||
|
models = []
|
||||||
|
|
||||||
|
if installed:
|
||||||
|
|
||||||
|
available_updates = flatpak.list_updates_as_str()
|
||||||
|
|
||||||
|
for app_json in installed:
|
||||||
|
model = self._map_to_model(app_json=app_json, installed=True, disk_loader=disk_loader)
|
||||||
|
model.update = app_json['id'] in available_updates
|
||||||
|
models.append(model)
|
||||||
|
|
||||||
|
return SearchResult(models, None, len(models))
|
||||||
|
|
||||||
|
def downgrade(self, pkg: FlatpakApplication, root_password: str, watcher: ProcessWatcher) -> bool:
|
||||||
|
watcher.change_progress(10)
|
||||||
|
watcher.change_substatus(self.i18n['flatpak.downgrade.commits'])
|
||||||
|
commits = flatpak.get_app_commits(pkg.ref, pkg.origin)
|
||||||
|
|
||||||
|
commit_idx = commits.index(pkg.commit)
|
||||||
|
|
||||||
|
# downgrade is not possible if the app current commit in the first one:
|
||||||
|
if commit_idx == len(commits) - 1:
|
||||||
|
watcher.show_message(self.i18n['flatpak.downgrade.impossible.title'], self.i18n['flatpak.downgrade.impossible.body'], MessageType.WARNING)
|
||||||
|
return False
|
||||||
|
|
||||||
|
commit = commits[commit_idx + 1]
|
||||||
|
watcher.change_substatus(self.i18n['flatpak.downgrade.reverting'])
|
||||||
|
watcher.change_progress(50)
|
||||||
|
success = ProcessHandler(watcher).handle(SystemProcess(subproc=flatpak.downgrade(pkg.ref, commit, root_password), success_phrase='Updates complete.'))
|
||||||
|
watcher.change_progress(100)
|
||||||
|
return success
|
||||||
|
|
||||||
|
def clean_cache_for(self, pkg: FlatpakApplication):
|
||||||
|
super(FlatpakManager, self).clean_cache_for(pkg)
|
||||||
|
self.api_cache.delete(pkg.id)
|
||||||
|
|
||||||
|
def update(self, pkg: FlatpakApplication, root_password: str, watcher: ProcessWatcher) -> bool:
|
||||||
|
return ProcessHandler(watcher).handle(SystemProcess(subproc=flatpak.update(pkg.ref)))
|
||||||
|
|
||||||
|
def uninstall(self, pkg: FlatpakApplication, root_password: str, watcher: ProcessWatcher) -> bool:
|
||||||
|
return ProcessHandler(watcher).handle(SystemProcess(subproc=flatpak.uninstall(pkg.ref)))
|
||||||
|
|
||||||
|
def get_info(self, app: FlatpakApplication) -> dict:
|
||||||
|
app_info = flatpak.get_app_info_fields(app.id, app.branch)
|
||||||
|
app_info['name'] = app.name
|
||||||
|
app_info['type'] = 'runtime' if app.runtime else 'app'
|
||||||
|
app_info['description'] = strip_html(app.description)
|
||||||
|
return app_info
|
||||||
|
|
||||||
|
def get_history(self, pkg: FlatpakApplication) -> PackageHistory:
|
||||||
|
commits = flatpak.get_app_commits_data(pkg.ref, pkg.origin)
|
||||||
|
status_idx = 0
|
||||||
|
|
||||||
|
for idx, data in enumerate(commits):
|
||||||
|
if data['commit'] == pkg.commit:
|
||||||
|
status_idx = idx
|
||||||
|
break
|
||||||
|
|
||||||
|
return PackageHistory(pkg=pkg, history=commits, pkg_status_idx=status_idx)
|
||||||
|
|
||||||
|
def install(self, pkg: FlatpakApplication, root_password: str, watcher: ProcessWatcher) -> bool:
|
||||||
|
return ProcessHandler(watcher).handle(SystemProcess(subproc=flatpak.install(pkg.id, pkg.origin)))
|
||||||
|
|
||||||
|
def is_enabled(self):
|
||||||
|
return flatpak.is_installed()
|
||||||
|
|
||||||
|
def requires_root(self, action: str, pkg: FlatpakApplication):
|
||||||
|
return action == 'downgrade'
|
||||||
|
|
||||||
|
def prepare(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def list_updates(self) -> List[PackageUpdate]:
|
||||||
|
updates = []
|
||||||
|
installed = flatpak.list_installed(extra_fields=False)
|
||||||
|
|
||||||
|
if installed:
|
||||||
|
available_updates = flatpak.list_updates_as_str()
|
||||||
|
|
||||||
|
if available_updates:
|
||||||
|
loaders = None
|
||||||
|
|
||||||
|
for app_json in installed:
|
||||||
|
if app_json['id'] in available_updates:
|
||||||
|
loader = FlatpakUpdateLoader(app=app_json, http_client=self.context.http_client)
|
||||||
|
loader.start()
|
||||||
|
|
||||||
|
if loaders is None:
|
||||||
|
loaders = []
|
||||||
|
|
||||||
|
loaders.append(loader)
|
||||||
|
|
||||||
|
if loaders:
|
||||||
|
for loader in loaders:
|
||||||
|
loader.join()
|
||||||
|
app = loader.app
|
||||||
|
updates.append(PackageUpdate(pkg_id='{}:{}'.format(app['id'], app['branch']),
|
||||||
|
pkg_type='flatpak',
|
||||||
|
version=app.get('version')))
|
||||||
|
|
||||||
|
return updates
|
||||||
|
|
||||||
|
def list_warnings(self) -> List[str]:
|
||||||
|
if flatpak.is_installed():
|
||||||
|
if not flatpak.has_remotes_set():
|
||||||
|
return [self.i18n['flatpak.notification.no_remotes']]
|
||||||
|
|
||||||
|
def list_suggestions(self, limit: int) -> List[PackageSuggestion]:
|
||||||
|
|
||||||
|
res = []
|
||||||
|
|
||||||
|
sugs = [(i, p) for i, p in suggestions.ALL.items()]
|
||||||
|
sugs.sort(key=lambda t: t[1].value, reverse=True)
|
||||||
|
|
||||||
|
for sug in sugs:
|
||||||
|
|
||||||
|
if limit <= 0 or len(res) < limit:
|
||||||
|
app_json = flatpak.search(sug[0], app_id=True)
|
||||||
|
|
||||||
|
if app_json:
|
||||||
|
res.append(PackageSuggestion(self._map_to_model(app_json[0], False, None), sug[1]))
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
return res
|
||||||
245
bauh/gems/flatpak/flatpak.py
Executable file
245
bauh/gems/flatpak/flatpak.py
Executable file
@@ -0,0 +1,245 @@
|
|||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from bauh.api.exception import NoInternetException
|
||||||
|
from bauh.commons.system import new_subprocess, run_cmd, new_root_subprocess
|
||||||
|
|
||||||
|
BASE_CMD = 'flatpak'
|
||||||
|
|
||||||
|
|
||||||
|
def app_str_to_json(line: str, version: str, extra_fields: bool = True) -> dict:
|
||||||
|
|
||||||
|
app_array = line.split('\t')
|
||||||
|
|
||||||
|
if version >= '1.3.0':
|
||||||
|
app = {'name': app_array[0],
|
||||||
|
'id': app_array[1],
|
||||||
|
'version': app_array[2],
|
||||||
|
'branch': app_array[3]}
|
||||||
|
|
||||||
|
elif '1.0' <= version < '1.1':
|
||||||
|
|
||||||
|
ref_data = app_array[0].split('/')
|
||||||
|
|
||||||
|
app = {'id': ref_data[0],
|
||||||
|
'arch': ref_data[1],
|
||||||
|
'name': ref_data[0].split('.')[-1],
|
||||||
|
'ref': app_array[0],
|
||||||
|
'options': app_array[1],
|
||||||
|
'branch': ref_data[2],
|
||||||
|
'version': None}
|
||||||
|
elif '1.2' <= version < '1.3':
|
||||||
|
app = {'id': app_array[1],
|
||||||
|
'name': app_array[1].strip().split('.')[-1],
|
||||||
|
'version': app_array[2],
|
||||||
|
'branch': app_array[3],
|
||||||
|
'arch': app_array[4],
|
||||||
|
'origin': app_array[5]}
|
||||||
|
else:
|
||||||
|
raise Exception('Unsupported version')
|
||||||
|
|
||||||
|
if extra_fields:
|
||||||
|
extra_fields = get_app_info_fields(app['id'], app['branch'], ['origin', 'arch', 'ref', 'commit'], check_runtime=True)
|
||||||
|
app.update(extra_fields)
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
def get_app_info_fields(app_id: str, branch: str, fields: List[str] = [], check_runtime: bool = False):
|
||||||
|
info = re.findall(r'\w+:\s.+', get_app_info(app_id, branch))
|
||||||
|
data = {}
|
||||||
|
fields_to_retrieve = len(fields) + (1 if check_runtime and 'ref' not in fields else 0)
|
||||||
|
|
||||||
|
for field in info:
|
||||||
|
|
||||||
|
if fields and fields_to_retrieve == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
field_val = field.split(':')
|
||||||
|
field_name = field_val[0].lower()
|
||||||
|
|
||||||
|
if not fields or field_name in fields or (check_runtime and field_name == 'ref'):
|
||||||
|
data[field_name] = field_val[1].strip()
|
||||||
|
|
||||||
|
if fields:
|
||||||
|
fields_to_retrieve -= 1
|
||||||
|
|
||||||
|
if check_runtime and field_name == 'ref':
|
||||||
|
data['runtime'] = data['ref'].startswith('runtime/')
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def is_installed():
|
||||||
|
version = get_version()
|
||||||
|
return False if version is None else True
|
||||||
|
|
||||||
|
|
||||||
|
def get_version():
|
||||||
|
res = run_cmd('{} --version'.format(BASE_CMD), print_error=False)
|
||||||
|
return res.split(' ')[1].strip() if res else None
|
||||||
|
|
||||||
|
|
||||||
|
def get_app_info(app_id: str, branch: str):
|
||||||
|
return run_cmd('{} info {} {}'.format(BASE_CMD, app_id, branch))
|
||||||
|
|
||||||
|
|
||||||
|
def list_installed(extra_fields: bool = True) -> List[dict]:
|
||||||
|
apps_str = run_cmd('{} list'.format(BASE_CMD))
|
||||||
|
|
||||||
|
if apps_str:
|
||||||
|
version = get_version()
|
||||||
|
app_lines = apps_str.split('\n')
|
||||||
|
return [app_str_to_json(line, version, extra_fields=extra_fields) for line in app_lines if line]
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def update(app_ref: str):
|
||||||
|
"""
|
||||||
|
Updates the app reference
|
||||||
|
:param app_ref:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return new_subprocess([BASE_CMD, 'update', '-y', app_ref])
|
||||||
|
|
||||||
|
|
||||||
|
def uninstall(app_ref: str):
|
||||||
|
"""
|
||||||
|
Removes the app by its reference
|
||||||
|
:param app_ref:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return new_subprocess([BASE_CMD, 'uninstall', app_ref, '-y'])
|
||||||
|
|
||||||
|
|
||||||
|
def list_updates_as_str():
|
||||||
|
return run_cmd('{} update'.format(BASE_CMD), ignore_return_code=True)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade(app_ref: str, commit: str, root_password: str) -> subprocess.Popen:
|
||||||
|
return new_root_subprocess([BASE_CMD, 'update', '--commit={}'.format(commit), app_ref, '-y'], root_password)
|
||||||
|
|
||||||
|
|
||||||
|
def get_app_commits(app_ref: str, origin: str) -> List[str]:
|
||||||
|
log = run_cmd('{} remote-info --log {} {}'.format(BASE_CMD, origin, app_ref))
|
||||||
|
|
||||||
|
if log:
|
||||||
|
return re.findall(r'Commit+:\s(.+)', log)
|
||||||
|
else:
|
||||||
|
raise NoInternetException()
|
||||||
|
|
||||||
|
|
||||||
|
def get_app_commits_data(app_ref: str, origin: str) -> List[dict]:
|
||||||
|
log = run_cmd('{} remote-info --log {} {}'.format(BASE_CMD, origin, app_ref))
|
||||||
|
|
||||||
|
if not log:
|
||||||
|
raise NoInternetException()
|
||||||
|
|
||||||
|
res = re.findall(r'(Commit|Subject|Date):\s(.+)', log)
|
||||||
|
|
||||||
|
commits = []
|
||||||
|
|
||||||
|
commit = {}
|
||||||
|
|
||||||
|
for idx, data in enumerate(res):
|
||||||
|
commit[data[0].strip().lower()] = data[1].strip()
|
||||||
|
|
||||||
|
if (idx + 1) % 3 == 0:
|
||||||
|
commits.append(commit)
|
||||||
|
commit = {}
|
||||||
|
|
||||||
|
return commits
|
||||||
|
|
||||||
|
|
||||||
|
def search(word: str, app_id: bool = False) -> List[dict]:
|
||||||
|
cli_version = get_version()
|
||||||
|
|
||||||
|
res = run_cmd('{} search {}'.format(BASE_CMD, word))
|
||||||
|
|
||||||
|
found = []
|
||||||
|
|
||||||
|
split_res = res.split('\n')
|
||||||
|
|
||||||
|
if split_res and split_res[0].lower() != 'no matches found':
|
||||||
|
for info in split_res:
|
||||||
|
if info:
|
||||||
|
info_list = info.split('\t')
|
||||||
|
if cli_version >= '1.3.0':
|
||||||
|
id_ = info_list[2].strip()
|
||||||
|
|
||||||
|
if app_id and id_ != word:
|
||||||
|
continue
|
||||||
|
|
||||||
|
version = info_list[3].strip()
|
||||||
|
app = {
|
||||||
|
'name': info_list[0].strip(),
|
||||||
|
'description': info_list[1].strip(),
|
||||||
|
'id': id_,
|
||||||
|
'version': version,
|
||||||
|
'latest_version': version,
|
||||||
|
'branch': info_list[4].strip(),
|
||||||
|
'origin': info_list[5].strip(),
|
||||||
|
'runtime': False,
|
||||||
|
'arch': None, # unknown at this moment,
|
||||||
|
'ref': None # unknown at this moment
|
||||||
|
}
|
||||||
|
elif cli_version >= '1.2.0':
|
||||||
|
id_ = info_list[1].strip()
|
||||||
|
|
||||||
|
if app_id and id_ != word:
|
||||||
|
continue
|
||||||
|
|
||||||
|
desc = info_list[0].split('-')
|
||||||
|
version = info_list[2].strip()
|
||||||
|
app = {
|
||||||
|
'name': desc[0].strip(),
|
||||||
|
'description': desc[1].strip(),
|
||||||
|
'id': id_,
|
||||||
|
'version': version,
|
||||||
|
'latest_version': version,
|
||||||
|
'branch': info_list[3].strip(),
|
||||||
|
'origin': info_list[4].strip(),
|
||||||
|
'runtime': False,
|
||||||
|
'arch': None, # unknown at this moment,
|
||||||
|
'ref': None # unknown at this moment
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
id_ = info_list[0].strip()
|
||||||
|
|
||||||
|
if app_id and id_ != word:
|
||||||
|
continue
|
||||||
|
|
||||||
|
version = info_list[1].strip()
|
||||||
|
app = {
|
||||||
|
'name': '',
|
||||||
|
'description': info_list[4].strip(),
|
||||||
|
'id': id_,
|
||||||
|
'version': version,
|
||||||
|
'latest_version': version,
|
||||||
|
'branch': info_list[2].strip(),
|
||||||
|
'origin': info_list[3].strip(),
|
||||||
|
'runtime': False,
|
||||||
|
'arch': None, # unknown at this moment,
|
||||||
|
'ref': None # unknown at this moment
|
||||||
|
}
|
||||||
|
|
||||||
|
found.append(app)
|
||||||
|
|
||||||
|
if app_id and len(found) > 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def install(app_id: str, origin: str):
|
||||||
|
return new_subprocess([BASE_CMD, 'install', origin, app_id, '-y'])
|
||||||
|
|
||||||
|
|
||||||
|
def set_default_remotes():
|
||||||
|
run_cmd('{} remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo'.format(BASE_CMD))
|
||||||
|
|
||||||
|
|
||||||
|
def has_remotes_set() -> bool:
|
||||||
|
return bool(run_cmd('{} remotes'.format(BASE_CMD)).strip())
|
||||||
69
bauh/gems/flatpak/model.py
Normal file
69
bauh/gems/flatpak/model.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
from bauh.api.abstract.model import SoftwarePackage
|
||||||
|
|
||||||
|
from bauh.commons import resource
|
||||||
|
from bauh.gems.flatpak import ROOT_DIR
|
||||||
|
|
||||||
|
|
||||||
|
class FlatpakApplication(SoftwarePackage):
|
||||||
|
|
||||||
|
def __init__(self, id: str = None, name: str = None, version: str = None, latest_version: str = None, description: str = None,
|
||||||
|
branch: str = None, arch: str = None, origin: str = None, runtime: bool = False, ref: str = str, commit: str = str):
|
||||||
|
super(FlatpakApplication, self).__init__(id=id, name=name, version=version,
|
||||||
|
latest_version=latest_version, description=description)
|
||||||
|
self.ref = ref
|
||||||
|
self.branch = branch
|
||||||
|
self.arch = arch
|
||||||
|
self.origin = origin
|
||||||
|
self.runtime = runtime
|
||||||
|
self.commit = commit
|
||||||
|
|
||||||
|
def is_incomplete(self):
|
||||||
|
return self.description is None and self.icon_url
|
||||||
|
|
||||||
|
def has_history(self):
|
||||||
|
return self.installed
|
||||||
|
|
||||||
|
def has_info(self):
|
||||||
|
return self.installed
|
||||||
|
|
||||||
|
def can_be_downgraded(self):
|
||||||
|
return self.installed
|
||||||
|
|
||||||
|
def get_type(self):
|
||||||
|
return 'flatpak'
|
||||||
|
|
||||||
|
def get_default_icon_path(self):
|
||||||
|
return resource.get_path('img/flathub.svg', ROOT_DIR)
|
||||||
|
|
||||||
|
def get_type_icon_path(self):
|
||||||
|
return self.get_default_icon_path()
|
||||||
|
|
||||||
|
def is_application(self):
|
||||||
|
return not self.runtime
|
||||||
|
|
||||||
|
def get_disk_cache_path(self):
|
||||||
|
return super(FlatpakApplication, self).get_disk_cache_path() + '/installed/' + self.id
|
||||||
|
|
||||||
|
def get_data_to_cache(self):
|
||||||
|
return {
|
||||||
|
'description': self.description,
|
||||||
|
'icon_url': self.icon_url,
|
||||||
|
'latest_version': self.latest_version,
|
||||||
|
'version': self.version,
|
||||||
|
'name': self.name,
|
||||||
|
'categories': self.categories
|
||||||
|
}
|
||||||
|
|
||||||
|
def fill_cached_data(self, data: dict):
|
||||||
|
for attr in self.get_data_to_cache().keys():
|
||||||
|
if data.get(attr) and not getattr(self, attr):
|
||||||
|
setattr(self, attr, data[attr])
|
||||||
|
|
||||||
|
def get_command(self) -> str:
|
||||||
|
return "flatpak run {}".format(self.id)
|
||||||
|
|
||||||
|
def can_be_run(self) -> bool:
|
||||||
|
return self.installed and not self.runtime
|
||||||
|
|
||||||
|
def get_publisher(self):
|
||||||
|
return 'flathub'
|
||||||
179
bauh/gems/flatpak/resources/img/flathub.svg
Executable file
179
bauh/gems/flatpak/resources/img/flathub.svg
Executable file
@@ -0,0 +1,179 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="47.919052"
|
||||||
|
height="48.110374"
|
||||||
|
viewBox="0 0 12.678582 12.729203"
|
||||||
|
version="1.1"
|
||||||
|
id="svg3871"
|
||||||
|
inkscape:version="0.92.3 (2405546, 2018-03-11)"
|
||||||
|
sodipodi:docname="flathub.svg">
|
||||||
|
<defs
|
||||||
|
id="defs3865" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="0.35"
|
||||||
|
inkscape:cx="8.4698372"
|
||||||
|
inkscape:cy="118.04149"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false"
|
||||||
|
showborder="false"
|
||||||
|
inkscape:window-width="1366"
|
||||||
|
inkscape:window-height="703"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="260"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
fit-margin-bottom="-0.2"
|
||||||
|
fit-margin-top="0"
|
||||||
|
fit-margin-left="0"
|
||||||
|
fit-margin-right="0"
|
||||||
|
units="px" />
|
||||||
|
<metadata
|
||||||
|
id="metadata3868">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(-12.122116,-167.33593)">
|
||||||
|
<g
|
||||||
|
transform="matrix(0.08875425,0,0,0.11490723,-57.513376,72.576168)"
|
||||||
|
id="g10674"
|
||||||
|
style="stroke-width:0.77710575">
|
||||||
|
<path
|
||||||
|
sodipodi:nodetypes="ccccccccccc"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path10642"
|
||||||
|
d="m 856.01397,830.22631 -66.76365,43.44891 v 34.93973 l 33.38182,21.72315 33.37926,-21.72315 v -0.005 l 0.003,0.003 33.38183,21.72574 33.37925,-21.72315 v -34.93979 z"
|
||||||
|
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#204a87;fill-opacity:1;fill-rule:nonzero;stroke:#204a87;stroke-width:9.3252697;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
|
||||||
|
<g
|
||||||
|
style="stroke-width:0.58728963"
|
||||||
|
transform="matrix(0.50221552,0,0,0.50636711,429.63205,432.90461)"
|
||||||
|
id="g10652">
|
||||||
|
<rect
|
||||||
|
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#204a87;fill-opacity:1;fill-rule:nonzero;stroke:#204a87;stroke-width:2.4608953;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||||
|
id="rect10644"
|
||||||
|
width="79.110413"
|
||||||
|
height="79.110413"
|
||||||
|
x="1289.9076"
|
||||||
|
y="279.42584"
|
||||||
|
rx="0"
|
||||||
|
ry="0"
|
||||||
|
transform="matrix(0.84019328,0.54228706,-0.84019328,0.54228706,0,0)" />
|
||||||
|
<path
|
||||||
|
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#204a87;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:2.34915853;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||||
|
d="m 915.46811,824.92983 -5e-5,68.99997 -66.46803,42.90055 5e-5,-68.99997 z"
|
||||||
|
id="path10648"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path10650"
|
||||||
|
d="m 783.00003,824.92983 5e-5,68.99997 66.46803,42.90055 -5e-5,-68.99997 z"
|
||||||
|
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:2.34915853;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
|
||||||
|
<rect
|
||||||
|
transform="matrix(0.84019328,0.54228706,-0.84019328,0.54228706,0,0)"
|
||||||
|
ry="0"
|
||||||
|
rx="0"
|
||||||
|
y="215.8064"
|
||||||
|
x="1226.2882"
|
||||||
|
height="79.110413"
|
||||||
|
width="79.110413"
|
||||||
|
id="rect10646"
|
||||||
|
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#204a87;stroke-width:2.4608953;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
style="stroke-width:0.58728963"
|
||||||
|
transform="matrix(0.50221552,0,0,0.50636711,463.01368,454.62849)"
|
||||||
|
id="g10662">
|
||||||
|
<rect
|
||||||
|
transform="matrix(0.84019328,0.54228706,-0.84019328,0.54228706,0,0)"
|
||||||
|
ry="0"
|
||||||
|
rx="0"
|
||||||
|
y="279.42584"
|
||||||
|
x="1289.9076"
|
||||||
|
height="79.110413"
|
||||||
|
width="79.110413"
|
||||||
|
id="rect10654"
|
||||||
|
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#204a87;fill-opacity:1;fill-rule:nonzero;stroke:#204a87;stroke-width:2.4608953;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
|
||||||
|
<rect
|
||||||
|
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#204a87;stroke-width:2.4608953;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||||
|
id="rect10656"
|
||||||
|
width="79.110413"
|
||||||
|
height="79.110413"
|
||||||
|
x="1226.2882"
|
||||||
|
y="215.8064"
|
||||||
|
rx="0"
|
||||||
|
ry="0"
|
||||||
|
transform="matrix(0.84019328,0.54228706,-0.84019328,0.54228706,0,0)" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path10658"
|
||||||
|
d="m 915.46811,824.92983 -5e-5,68.99997 -66.46803,42.90055 5e-5,-68.99997 z"
|
||||||
|
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#204a87;fill-opacity:1;fill-rule:nonzero;stroke:#204a87;stroke-width:2.34915853;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
|
||||||
|
<path
|
||||||
|
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#204a87;stroke-width:2.34915853;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||||
|
d="m 783.00003,826.54445 5e-5,68.99997 66.46803,42.90055 -5e-5,-68.99997 z"
|
||||||
|
id="path10660"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
style="stroke-width:0.58728963"
|
||||||
|
id="g10672"
|
||||||
|
transform="matrix(0.50221552,0,0,0.50636711,396.25042,454.62849)">
|
||||||
|
<rect
|
||||||
|
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#204a87;fill-opacity:1;fill-rule:nonzero;stroke:#204a87;stroke-width:2.4608953;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||||
|
id="rect10664"
|
||||||
|
width="79.110413"
|
||||||
|
height="79.110413"
|
||||||
|
x="1289.9076"
|
||||||
|
y="279.42584"
|
||||||
|
rx="0"
|
||||||
|
ry="0"
|
||||||
|
transform="matrix(0.84019328,0.54228706,-0.84019328,0.54228706,0,0)" />
|
||||||
|
<rect
|
||||||
|
transform="matrix(0.84019328,0.54228706,-0.84019328,0.54228706,0,0)"
|
||||||
|
ry="0"
|
||||||
|
rx="0"
|
||||||
|
y="215.8064"
|
||||||
|
x="1226.2882"
|
||||||
|
height="79.110413"
|
||||||
|
width="79.110413"
|
||||||
|
id="rect10666"
|
||||||
|
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#204a87;stroke-width:2.4608953;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
|
||||||
|
<path
|
||||||
|
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#204a87;fill-opacity:1;fill-rule:nonzero;stroke:#204a87;stroke-width:2.34915853;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||||
|
d="m 915.46811,824.92983 -5e-5,68.99997 -66.46803,42.90055 5e-5,-68.99997 z"
|
||||||
|
id="path10668"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path10670"
|
||||||
|
d="m 783.00003,824.92983 5e-5,68.99997 66.46803,42.90055 -5e-5,-68.99997 z"
|
||||||
|
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#204a87;stroke-width:2.34915853;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 14 KiB |
BIN
bauh/gems/flatpak/resources/img/flatpak.png
Executable file
BIN
bauh/gems/flatpak/resources/img/flatpak.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 8.7 KiB |
5
bauh/gems/flatpak/resources/locale/en
Normal file
5
bauh/gems/flatpak/resources/locale/en
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
flatpak.notification.no_remotes=No Flatpak remotes set. It will not be possible to search for Flatpak apps.
|
||||||
|
flatpak.downgrade.impossible.title=Error
|
||||||
|
flatpak.downgrade.impossible.body=Impossible to downgrade: the app is in its first version
|
||||||
|
flatpak.downgrade.commits=Reading package commits
|
||||||
|
flatpak.downgrade.reverting=Reverting version
|
||||||
27
bauh/gems/flatpak/resources/locale/es
Normal file
27
bauh/gems/flatpak/resources/locale/es
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
flatpak.info.arch=arquitectura
|
||||||
|
flatpak.info.branch=rama
|
||||||
|
flatpak.info.collection=colección
|
||||||
|
flatpak.info.commit=commit
|
||||||
|
flatpak.info.date=fecha
|
||||||
|
flatpak.info.description=descripción
|
||||||
|
flatpak.info.id=id
|
||||||
|
flatpak.info.installation=instalación
|
||||||
|
flatpak.info.installed=instalado
|
||||||
|
flatpak.info.license=licencia
|
||||||
|
flatpak.info.name=nombre
|
||||||
|
flatpak.info.origin=origen
|
||||||
|
flatpak.info.parent=padre
|
||||||
|
flatpak.info.ref=ref
|
||||||
|
flatpak.info.runtime=runtime
|
||||||
|
flatpak.info.sdk=sdk
|
||||||
|
flatpak.info.subject=tema
|
||||||
|
flatpak.info.type=tipo
|
||||||
|
flatpak.info.version=versión
|
||||||
|
flatpak.notification.no_remotes=No hay repositorios (remotes) Flatpak configurados. No será posible buscar aplicativos Flatpak.
|
||||||
|
flatpak.downgrade.impossible.title=Error
|
||||||
|
flatpak.downgrade.impossible.body=Imposible revertir la versión: el aplicativo está en su primera versión
|
||||||
|
flatpak.history.date=fecha
|
||||||
|
flatpak.history.commit=commit
|
||||||
|
flatpak.history.subject=tema
|
||||||
|
flatpak.downgrade.commits=Leyendo commits del paquete
|
||||||
|
flatpak.downgrade.reverting=Revirtiendo la versión
|
||||||
27
bauh/gems/flatpak/resources/locale/pt
Normal file
27
bauh/gems/flatpak/resources/locale/pt
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
flatpak.info.arch=arquitetura
|
||||||
|
flatpak.info.branch=ramo
|
||||||
|
flatpak.info.collection=coleção
|
||||||
|
flatpak.info.commit=commit
|
||||||
|
flatpak.info.date=data
|
||||||
|
flatpak.info.description=descrição
|
||||||
|
flatpak.info.id=id
|
||||||
|
flatpak.info.installation=instalação
|
||||||
|
flatpak.info.installed=instalado
|
||||||
|
flatpak.info.license=licença
|
||||||
|
flatpak.info.name=nome
|
||||||
|
flatpak.info.origin=origem
|
||||||
|
flatpak.info.parent=pai
|
||||||
|
flatpak.info.ref=ref
|
||||||
|
flatpak.info.runtime=runtime
|
||||||
|
flatpak.info.sdk=sdk
|
||||||
|
flatpak.info.subject=assunto
|
||||||
|
flatpak.info.type=tipo
|
||||||
|
flatpak.info.version=versão
|
||||||
|
flatpak.notification.no_remotes=Não há repositórios (remotes) Flatpak configurados. Não será possível buscar aplicativos Flatpak.
|
||||||
|
flatpak.downgrade.impossible.title=Erro
|
||||||
|
flatpak.downgrade.impossible.body=Impossível reverter a versão: o aplicativo está na sua primeira versão
|
||||||
|
flatpak.history.date=data
|
||||||
|
flatpak.history.commit=commit
|
||||||
|
flatpak.history.subject=assunto
|
||||||
|
flatpak.downgrade.commits=Lendo os commits do pacote
|
||||||
|
flatpak.downgrade.reverting=Revertendo a versão
|
||||||
15
bauh/gems/flatpak/suggestions.py
Normal file
15
bauh/gems/flatpak/suggestions.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from bauh.api.abstract.model import SuggestionPriority
|
||||||
|
|
||||||
|
ALL = {
|
||||||
|
'com.spotify.Client': SuggestionPriority.TOP,
|
||||||
|
'com.skype.Client': SuggestionPriority.HIGH,
|
||||||
|
'com.dropbox.Client': SuggestionPriority.MEDIUM,
|
||||||
|
'us.zoom.Zoom': SuggestionPriority.MEDIUM,
|
||||||
|
'org.telegram.desktop': SuggestionPriority.MEDIUM,
|
||||||
|
'com.visualstudio.code': SuggestionPriority.LOW,
|
||||||
|
'org.inkscape.Inkscape': SuggestionPriority.LOW,
|
||||||
|
'org.libretro.RetroArch': SuggestionPriority.LOW,
|
||||||
|
'org.kde.kdenlive': SuggestionPriority.LOW,
|
||||||
|
'org.videolan.VLC': SuggestionPriority.LOW
|
||||||
|
}
|
||||||
|
|
||||||
96
bauh/gems/flatpak/worker.py
Normal file
96
bauh/gems/flatpak/worker.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import traceback
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
from bauh.api.abstract.cache import MemoryCache
|
||||||
|
from bauh.api.abstract.context import ApplicationContext
|
||||||
|
from bauh.api.abstract.controller import SoftwareManager
|
||||||
|
from bauh.api.abstract.model import PackageStatus
|
||||||
|
from bauh.api.http import HttpClient
|
||||||
|
|
||||||
|
from bauh.gems.flatpak import flatpak
|
||||||
|
from bauh.gems.flatpak.constants import FLATHUB_API_URL, FLATHUB_URL
|
||||||
|
from bauh.gems.flatpak.model import FlatpakApplication
|
||||||
|
|
||||||
|
|
||||||
|
class FlatpakAsyncDataLoader(Thread):
|
||||||
|
|
||||||
|
def __init__(self, app: FlatpakApplication, manager: SoftwareManager, context: ApplicationContext, api_cache: MemoryCache):
|
||||||
|
super(FlatpakAsyncDataLoader, self).__init__(daemon=True)
|
||||||
|
self.app = app
|
||||||
|
self.manager = manager
|
||||||
|
self.http_client = context.http_client
|
||||||
|
self.api_cache = api_cache
|
||||||
|
self.persist = False
|
||||||
|
self.logger = context.logger
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
if self.app:
|
||||||
|
self.app.status = PackageStatus.LOADING_DATA
|
||||||
|
|
||||||
|
try:
|
||||||
|
res = self.http_client.get('{}/apps/{}'.format(FLATHUB_API_URL, self.app.id))
|
||||||
|
|
||||||
|
if res.status_code == 200 and res.text:
|
||||||
|
data = res.json()
|
||||||
|
|
||||||
|
if not self.app.version:
|
||||||
|
self.app.version = data.get('version')
|
||||||
|
|
||||||
|
if not self.app.name:
|
||||||
|
self.app.name = data.get('name')
|
||||||
|
|
||||||
|
self.app.description = data.get('description', data.get('summary', None))
|
||||||
|
self.app.icon_url = data.get('iconMobileUrl', None)
|
||||||
|
self.app.latest_version = data.get('currentReleaseVersion', self.app.version)
|
||||||
|
|
||||||
|
if not self.app.version and self.app.latest_version:
|
||||||
|
self.app.version = self.app.latest_version
|
||||||
|
|
||||||
|
if not self.app.installed and self.app.latest_version:
|
||||||
|
self.app.version = self.app.latest_version
|
||||||
|
|
||||||
|
if self.app.icon_url and self.app.icon_url.startswith('/'):
|
||||||
|
self.app.icon_url = FLATHUB_URL + self.app.icon_url
|
||||||
|
|
||||||
|
if data.get('categories'):
|
||||||
|
self.app.categories = [c['name'] for c in data['categories']]
|
||||||
|
|
||||||
|
loaded_data = self.app.get_data_to_cache()
|
||||||
|
|
||||||
|
self.api_cache.add(self.app.id, loaded_data)
|
||||||
|
self.persist = self.app.supports_disk_cache()
|
||||||
|
else:
|
||||||
|
self.logger.warning("Could not retrieve app data for id '{}'. Server response: {}. Body: {}".format(self.app.id, res.status_code, res.content.decode()))
|
||||||
|
except:
|
||||||
|
self.logger.error("Could not retrieve app data for id '{}'".format(self.app.id))
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
self.app.status = PackageStatus.READY
|
||||||
|
|
||||||
|
if self.persist:
|
||||||
|
self.manager.cache_to_disk(pkg=self.app, icon_bytes=None, only_icon=False)
|
||||||
|
|
||||||
|
|
||||||
|
class FlatpakUpdateLoader(Thread):
|
||||||
|
|
||||||
|
def __init__(self, app: dict, http_client: HttpClient):
|
||||||
|
super(FlatpakUpdateLoader, self).__init__(daemon=True)
|
||||||
|
self.app = app
|
||||||
|
self.http_client = http_client
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
|
||||||
|
if self.app.get('ref') is None:
|
||||||
|
self.app.update(flatpak.get_app_info_fields(self.app['id'], self.app['branch'], fields=['ref'], check_runtime=True))
|
||||||
|
else:
|
||||||
|
self.app['runtime'] = self.app['ref'].startswith('runtime/')
|
||||||
|
|
||||||
|
if not self.app['runtime']:
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = self.http_client.get_json('{}/apps/{}'.format(FLATHUB_API_URL, self.app['id']))
|
||||||
|
|
||||||
|
if data and data.get('currentReleaseVersion'):
|
||||||
|
self.app['version'] = data['currentReleaseVersion']
|
||||||
|
except:
|
||||||
|
traceback.print_exc()
|
||||||
2
bauh/gems/snap/__init__.py
Normal file
2
bauh/gems/snap/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import os
|
||||||
|
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
1
bauh/gems/snap/constants.py
Normal file
1
bauh/gems/snap/constants.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
SNAP_API_URL = 'https://search.apps.ubuntu.com/api/v1'
|
||||||
145
bauh/gems/snap/controller.py
Normal file
145
bauh/gems/snap/controller.py
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import List, Set, Type
|
||||||
|
|
||||||
|
from bauh.api.abstract.controller import SoftwareManager, SearchResult, ApplicationContext
|
||||||
|
from bauh.api.abstract.disk import DiskCacheLoader
|
||||||
|
from bauh.api.abstract.handler import ProcessWatcher
|
||||||
|
from bauh.api.abstract.model import SoftwarePackage, PackageHistory, PackageUpdate, PackageSuggestion
|
||||||
|
from bauh.commons.system import SystemProcess, ProcessHandler
|
||||||
|
|
||||||
|
from bauh.gems.snap import snap, suggestions
|
||||||
|
from bauh.gems.snap.model import SnapApplication
|
||||||
|
from bauh.gems.snap.worker import SnapAsyncDataLoader
|
||||||
|
|
||||||
|
|
||||||
|
class SnapManager(SoftwareManager):
|
||||||
|
|
||||||
|
def __init__(self, context: ApplicationContext):
|
||||||
|
super(SnapManager, self).__init__(context=context)
|
||||||
|
self.i18n = context.i18n
|
||||||
|
self.api_cache = context.cache_factory.new()
|
||||||
|
context.disk_loader_factory.map(SnapApplication, self.api_cache)
|
||||||
|
|
||||||
|
def map_json(self, app_json: dict, installed: bool, disk_loader: DiskCacheLoader) -> SnapApplication:
|
||||||
|
app = SnapApplication(publisher=app_json.get('publisher'),
|
||||||
|
rev=app_json.get('rev'),
|
||||||
|
notes=app_json.get('notes'),
|
||||||
|
app_type=app_json.get('type'),
|
||||||
|
id=app_json.get('name'),
|
||||||
|
name=app_json.get('name'),
|
||||||
|
version=app_json.get('version'),
|
||||||
|
latest_version=app_json.get('version'),
|
||||||
|
description=app_json.get('description'))
|
||||||
|
|
||||||
|
if app.publisher:
|
||||||
|
app.publisher = app.publisher.replace('*', '')
|
||||||
|
|
||||||
|
app.installed = installed
|
||||||
|
|
||||||
|
api_data = self.api_cache.get(app_json['name'])
|
||||||
|
expired_data = api_data and api_data.get('expires_at') and api_data['expires_at'] <= datetime.utcnow()
|
||||||
|
|
||||||
|
if (not api_data or expired_data) and app.is_application():
|
||||||
|
if disk_loader and app.installed:
|
||||||
|
disk_loader.fill(app)
|
||||||
|
|
||||||
|
SnapAsyncDataLoader(app=app, api_cache=self.api_cache, manager=self, context=self.context).start()
|
||||||
|
else:
|
||||||
|
app.fill_cached_data(api_data)
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
def search(self, words: str, disk_loader: DiskCacheLoader, limit: int = -1) -> SearchResult:
|
||||||
|
installed = self.read_installed(disk_loader).installed
|
||||||
|
|
||||||
|
res = SearchResult([], [], 0)
|
||||||
|
|
||||||
|
for app_json in snap.search(words):
|
||||||
|
|
||||||
|
already_installed = None
|
||||||
|
|
||||||
|
if installed:
|
||||||
|
already_installed = [i for i in installed if i.id == app_json.get('name')]
|
||||||
|
already_installed = already_installed[0] if already_installed else None
|
||||||
|
|
||||||
|
if already_installed:
|
||||||
|
res.installed.append(already_installed)
|
||||||
|
else:
|
||||||
|
res.new.append(self.map_json(app_json, installed=False, disk_loader=disk_loader))
|
||||||
|
|
||||||
|
res.total = len(res.installed) + len(res.new)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def read_installed(self, disk_loader: DiskCacheLoader, limit: int = -1, only_apps: bool = False, pkg_types: Set[Type[SoftwarePackage]] = None) -> SearchResult:
|
||||||
|
installed = [self.map_json(app_json, installed=True, disk_loader=disk_loader) for app_json in snap.read_installed()]
|
||||||
|
return SearchResult(installed, None, len(installed))
|
||||||
|
|
||||||
|
def downgrade(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher) -> bool:
|
||||||
|
return ProcessHandler(watcher).handle(SystemProcess(subproc=snap.downgrade_and_stream(pkg.name, root_password), wrong_error_phrase=None))
|
||||||
|
|
||||||
|
def update(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher) -> SystemProcess:
|
||||||
|
raise Exception("'update' is not supported by {}".format(pkg.__class__.__name__))
|
||||||
|
|
||||||
|
def uninstall(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher) -> bool:
|
||||||
|
return ProcessHandler(watcher).handle(SystemProcess(subproc=snap.uninstall_and_stream(pkg.name, root_password)))
|
||||||
|
|
||||||
|
def get_managed_types(self) -> Set[Type[SoftwarePackage]]:
|
||||||
|
return {SnapApplication}
|
||||||
|
|
||||||
|
def clean_cache_for(self, pkg: SnapApplication):
|
||||||
|
super(SnapManager, self).clean_cache_for(pkg)
|
||||||
|
self.api_cache.delete(pkg.id)
|
||||||
|
|
||||||
|
def get_info(self, pkg: SnapApplication) -> dict:
|
||||||
|
info = snap.get_info(pkg.name, attrs=('license', 'contact', 'commands', 'snap-id', 'tracking', 'installed'))
|
||||||
|
info['description'] = pkg.description
|
||||||
|
info['publisher'] = pkg.publisher
|
||||||
|
info['revision'] = pkg.rev
|
||||||
|
info['name'] = pkg.name
|
||||||
|
|
||||||
|
if info.get('commands'):
|
||||||
|
info['commands'] = ' '.join(info['commands'])
|
||||||
|
|
||||||
|
return info
|
||||||
|
|
||||||
|
def get_history(self, pkg: SnapApplication) -> PackageHistory:
|
||||||
|
raise Exception("'get_history' is not supported by {}".format(pkg.__class__.__name__))
|
||||||
|
|
||||||
|
def install(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher) -> bool:
|
||||||
|
return ProcessHandler(watcher).handle(SystemProcess(subproc=snap.install_and_stream(pkg.name, pkg.confinement, root_password)))
|
||||||
|
|
||||||
|
def is_enabled(self) -> bool:
|
||||||
|
return snap.is_installed()
|
||||||
|
|
||||||
|
def requires_root(self, action: str, pkg: SnapApplication):
|
||||||
|
return action != 'search'
|
||||||
|
|
||||||
|
def refresh(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher) -> bool:
|
||||||
|
return ProcessHandler(watcher).handle(SystemProcess(subproc=snap.refresh_and_stream(pkg.name, root_password)))
|
||||||
|
|
||||||
|
def prepare(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def list_updates(self) -> List[PackageUpdate]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def list_warnings(self) -> List[str]:
|
||||||
|
if snap.get_snapd_version() == 'unavailable':
|
||||||
|
return [self.i18n['snap.notification.snapd_unavailable']]
|
||||||
|
|
||||||
|
def list_suggestions(self, limit: int) -> List[PackageSuggestion]:
|
||||||
|
res = []
|
||||||
|
|
||||||
|
sugs = [(i, p) for i, p in suggestions.ALL.items()]
|
||||||
|
sugs.sort(key=lambda t: t[1].value, reverse=True)
|
||||||
|
|
||||||
|
for sug in sugs:
|
||||||
|
|
||||||
|
if limit <= 0 or len(res) < limit:
|
||||||
|
found = snap.search(sug[0], exact_name=True)
|
||||||
|
if found:
|
||||||
|
res.append(PackageSuggestion(self.map_json(found[0], installed=False, disk_loader=None), sug[1]))
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
return res
|
||||||
88
bauh/gems/snap/model.py
Normal file
88
bauh/gems/snap/model.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
from typing import List
|
||||||
|
|
||||||
|
from bauh.api.abstract.model import SoftwarePackage, PackageAction
|
||||||
|
from bauh.commons import resource
|
||||||
|
|
||||||
|
from bauh.gems.snap import ROOT_DIR
|
||||||
|
|
||||||
|
EXTRA_INSTALLED_ACTIONS = [
|
||||||
|
PackageAction(i18n_status_key='snap.action.refresh.status',
|
||||||
|
i18_label_key='snap.action.refresh.label',
|
||||||
|
icon_path=resource.get_path('img/refresh.svg', ROOT_DIR),
|
||||||
|
manager_method='refresh',
|
||||||
|
requires_root=True)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class SnapApplication(SoftwarePackage):
|
||||||
|
|
||||||
|
def __init__(self, id: str = None, name: str = None, version: str = None, latest_version: str = None,
|
||||||
|
description: str = None, publisher: str = None, rev: str = None, notes: str = None,
|
||||||
|
app_type: str = None, confinement: str = None):
|
||||||
|
super(SnapApplication, self).__init__(id=id, name=name, version=version,
|
||||||
|
latest_version=latest_version, description=description)
|
||||||
|
self.publisher = publisher
|
||||||
|
self.rev = rev
|
||||||
|
self.notes = notes
|
||||||
|
self.type = app_type
|
||||||
|
self.confinement = confinement
|
||||||
|
|
||||||
|
def has_history(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def has_info(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def can_be_downgraded(self):
|
||||||
|
return self.installed
|
||||||
|
|
||||||
|
def get_type(self):
|
||||||
|
return 'snap'
|
||||||
|
|
||||||
|
def get_default_icon_path(self):
|
||||||
|
return resource.get_path('img/snap.png', ROOT_DIR)
|
||||||
|
|
||||||
|
def get_type_icon_path(self):
|
||||||
|
return self.get_default_icon_path()
|
||||||
|
|
||||||
|
def is_application(self):
|
||||||
|
return not self.type and (self.type not in ('core', 'base', 'snapd') and not self._name_starts_with(('gtk-', 'gnome-', 'kde-', 'gtk2-')))
|
||||||
|
|
||||||
|
def _name_starts_with(self, words: tuple):
|
||||||
|
for word in words:
|
||||||
|
if self.name.startswith(word):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_disk_cache_path(self):
|
||||||
|
return super(SnapApplication, self).get_disk_cache_path() + '/installed/' + self.name
|
||||||
|
|
||||||
|
def get_data_to_cache(self):
|
||||||
|
return {
|
||||||
|
"icon_url": self.icon_url,
|
||||||
|
'confinement': self.confinement,
|
||||||
|
'description': self.description
|
||||||
|
}
|
||||||
|
|
||||||
|
def fill_cached_data(self, data: dict):
|
||||||
|
if data:
|
||||||
|
for base_attr in ('icon_url', 'description'):
|
||||||
|
if data.get(base_attr):
|
||||||
|
setattr(self, base_attr, data[base_attr])
|
||||||
|
|
||||||
|
if data.get('confinement'):
|
||||||
|
self.confinement = data['confinement']
|
||||||
|
|
||||||
|
def get_command(self) -> str:
|
||||||
|
return "snap run " + self.name
|
||||||
|
|
||||||
|
def can_be_run(self) -> bool:
|
||||||
|
return self.installed and self.is_application()
|
||||||
|
|
||||||
|
def get_publisher(self):
|
||||||
|
return self.publisher
|
||||||
|
|
||||||
|
def get_custom_supported_actions(self) -> List[PackageAction]:
|
||||||
|
if self.installed:
|
||||||
|
return EXTRA_INSTALLED_ACTIONS
|
||||||
146
bauh/gems/snap/resources/img/refresh.svg
Executable file
146
bauh/gems/snap/resources/img/refresh.svg
Executable file
@@ -0,0 +1,146 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
version="1.1"
|
||||||
|
id="Capa_1"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
width="48"
|
||||||
|
height="48"
|
||||||
|
viewBox="0 0 47.999999 47.999999"
|
||||||
|
xml:space="preserve"
|
||||||
|
sodipodi:docname="refresh_2.svg"
|
||||||
|
inkscape:version="0.92.3 (2405546, 2018-03-11)"><metadata
|
||||||
|
id="metadata45"><rdf:RDF><cc:Work
|
||||||
|
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||||
|
id="defs43"><linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
id="linearGradient540"><stop
|
||||||
|
style="stop-color:#0088aa;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop536" /><stop
|
||||||
|
style="stop-color:#0088aa;stop-opacity:0;"
|
||||||
|
offset="1"
|
||||||
|
id="stop538" /></linearGradient><linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient540"
|
||||||
|
id="linearGradient542"
|
||||||
|
x1="-130.74721"
|
||||||
|
y1="-214.80638"
|
||||||
|
x2="-556.49438"
|
||||||
|
y2="110.01643"
|
||||||
|
gradientUnits="userSpaceOnUse" /></defs><sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1366"
|
||||||
|
inkscape:window-height="703"
|
||||||
|
id="namedview41"
|
||||||
|
showgrid="false"
|
||||||
|
showborder="false"
|
||||||
|
inkscape:zoom="1.9374833"
|
||||||
|
inkscape:cx="165.58246"
|
||||||
|
inkscape:cy="-10.288149"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="260"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="Capa_1"
|
||||||
|
fit-margin-top="0"
|
||||||
|
fit-margin-left="0"
|
||||||
|
fit-margin-right="0"
|
||||||
|
fit-margin-bottom="0" />
|
||||||
|
<g
|
||||||
|
id="g8"
|
||||||
|
transform="matrix(0.10547014,0,0,0.0985161,-1.6940777,0)"
|
||||||
|
style="fill:url(#linearGradient542);fill-opacity:1">
|
||||||
|
<g
|
||||||
|
id="g6"
|
||||||
|
style="fill:url(#linearGradient542);fill-opacity:1">
|
||||||
|
<path
|
||||||
|
d="m 55.323,203.641 c 15.664,0 29.813,-9.405 35.872,-23.854 25.017,-59.604 83.842,-101.61 152.42,-101.61 37.797,0 72.449,12.955 100.23,34.442 l -21.775,3.371 c -7.438,1.153 -13.224,7.054 -14.232,14.512 -1.01,7.454 3.008,14.686 9.867,17.768 l 119.746,53.872 c 5.249,2.357 11.33,1.904 16.168,-1.205 4.83,-3.114 7.764,-8.458 7.796,-14.208 l 0.621,-131.943 c 0.042,-7.506 -4.851,-14.144 -12.024,-16.332 -7.185,-2.188 -14.947,0.589 -19.104,6.837 L 414.403,70.096 C 370.398,26.778 310.1,0 243.615,0 142.806,0 56.133,61.562 19.167,149.06 c -5.134,12.128 -3.84,26.015 3.429,36.987 7.269,10.976 19.556,17.594 32.727,17.594 z"
|
||||||
|
id="path2"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:url(#linearGradient542);fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
d="m 464.635,301.184 c -7.27,-10.977 -19.558,-17.594 -32.728,-17.594 -15.664,0 -29.813,9.405 -35.872,23.854 -25.018,59.604 -83.843,101.61 -152.42,101.61 -37.798,0 -72.45,-12.955 -100.232,-34.442 l 21.776,-3.369 c 7.437,-1.153 13.223,-7.055 14.233,-14.514 1.009,-7.453 -3.008,-14.686 -9.867,-17.768 L 49.779,285.089 c -5.25,-2.356 -11.33,-1.905 -16.169,1.205 -4.829,3.114 -7.764,8.458 -7.795,14.207 l -0.622,131.943 c -0.042,7.506 4.85,14.144 12.024,16.332 7.185,2.188 14.948,-0.59 19.104,-6.839 l 16.505,-24.805 c 44.004,43.32 104.303,70.098 170.788,70.098 100.811,0 187.481,-61.561 224.446,-149.059 5.137,-12.128 3.843,-26.014 -3.425,-36.987 z"
|
||||||
|
id="path4"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:url(#linearGradient542);fill-opacity:1" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g10"
|
||||||
|
transform="translate(-16.062155,-439.23)">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g12"
|
||||||
|
transform="translate(-16.062155,-439.23)">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g14"
|
||||||
|
transform="translate(-16.062155,-439.23)">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g16"
|
||||||
|
transform="translate(-16.062155,-439.23)">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g18"
|
||||||
|
transform="translate(-16.062155,-439.23)">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g20"
|
||||||
|
transform="translate(-16.062155,-439.23)">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g22"
|
||||||
|
transform="translate(-16.062155,-439.23)">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g24"
|
||||||
|
transform="translate(-16.062155,-439.23)">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g26"
|
||||||
|
transform="translate(-16.062155,-439.23)">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g28"
|
||||||
|
transform="translate(-16.062155,-439.23)">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g30"
|
||||||
|
transform="translate(-16.062155,-439.23)">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g32"
|
||||||
|
transform="translate(-16.062155,-439.23)">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g34"
|
||||||
|
transform="translate(-16.062155,-439.23)">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g36"
|
||||||
|
transform="translate(-16.062155,-439.23)">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g38"
|
||||||
|
transform="translate(-16.062155,-439.23)">
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.8 KiB |
BIN
bauh/gems/snap/resources/img/snap.png
Executable file
BIN
bauh/gems/snap/resources/img/snap.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
3
bauh/gems/snap/resources/locale/en
Normal file
3
bauh/gems/snap/resources/locale/en
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
snap.notification.snapd_unavailable=snapd seems not to be enabled. snap packages will not be available.
|
||||||
|
snap.action.refresh.status=Refreshing
|
||||||
|
snap.action.refresh.label=Refresh
|
||||||
12
bauh/gems/snap/resources/locale/es
Normal file
12
bauh/gems/snap/resources/locale/es
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
snap.info.commands=comandos
|
||||||
|
snap.info.contact=contacto
|
||||||
|
snap.info.description=descripción
|
||||||
|
snap.info.license=licencia
|
||||||
|
snap.info.license.unset=no está definida
|
||||||
|
snap.info.revision=revisión
|
||||||
|
snap.info.tracking=tracking
|
||||||
|
snap.info.installed=instalado
|
||||||
|
snap.info.publisher=publicador
|
||||||
|
snap.notification.snapd_unavailable=snapd no parece estar habilitado. los paquetes snap estarán indisponibles.
|
||||||
|
snap.action.refresh.status=Actualizando
|
||||||
|
snap.action.refresh.label=Actualizar
|
||||||
12
bauh/gems/snap/resources/locale/pt
Normal file
12
bauh/gems/snap/resources/locale/pt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
snap.info.commands=comandos
|
||||||
|
snap.info.contact=contato
|
||||||
|
snap.info.description=descrição
|
||||||
|
snap.info.license=licença
|
||||||
|
snap.info.license.unset=não definida
|
||||||
|
snap.info.revision=revisão
|
||||||
|
snap.info.tracking=tracking
|
||||||
|
snap.info.installed=instalado
|
||||||
|
snap.info.publisher=publicador
|
||||||
|
snap.notification.snapd_unavailable=snapd não parece estar habilitado. os pacotes snap estarão indisponíveis.
|
||||||
|
snap.action.refresh.status=Atualizando
|
||||||
|
snap.action.refresh.label=Atualizar
|
||||||
141
bauh/gems/snap/snap.py
Normal file
141
bauh/gems/snap/snap.py
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from bauh.commons.system import new_root_subprocess, run_cmd
|
||||||
|
|
||||||
|
BASE_CMD = 'snap'
|
||||||
|
|
||||||
|
|
||||||
|
def is_installed():
|
||||||
|
version = get_snapd_version()
|
||||||
|
return False if version is None else True
|
||||||
|
|
||||||
|
|
||||||
|
def get_version():
|
||||||
|
res = run_cmd('{} --version'.format(BASE_CMD), print_error=False)
|
||||||
|
return res.split('\n')[0].split(' ')[-1].strip() if res else None
|
||||||
|
|
||||||
|
|
||||||
|
def get_snapd_version():
|
||||||
|
res = run_cmd('{} --version'.format(BASE_CMD), print_error=False)
|
||||||
|
|
||||||
|
if not res:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
lines = res.split('\n')
|
||||||
|
|
||||||
|
if lines and len(lines) >= 2:
|
||||||
|
version = lines[1].split(' ')[-1].strip()
|
||||||
|
return version if version and version.lower() != 'unavailable' else None
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def app_str_to_json(app: str) -> dict:
|
||||||
|
app_data = [word for word in app.split(' ') if word]
|
||||||
|
app_json = {
|
||||||
|
'name': app_data[0],
|
||||||
|
'version': app_data[1],
|
||||||
|
'rev': app_data[2],
|
||||||
|
'tracking': app_data[3],
|
||||||
|
'publisher': app_data[4] if len(app_data) >= 5 else None,
|
||||||
|
'notes': app_data[5] if len(app_data) >= 6 else None
|
||||||
|
}
|
||||||
|
|
||||||
|
app_json.update(get_info(app_json['name'], ('summary', 'type', 'description')))
|
||||||
|
return app_json
|
||||||
|
|
||||||
|
|
||||||
|
def get_info(app_name: str, attrs: tuple = None):
|
||||||
|
full_info_lines = run_cmd('{} info {}'.format(BASE_CMD, app_name))
|
||||||
|
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
if full_info_lines:
|
||||||
|
re_attrs = r'\w+' if not attrs else '|'.join(attrs)
|
||||||
|
info_map = re.findall(r'({}):\s+(.+)'.format(re_attrs), full_info_lines)
|
||||||
|
|
||||||
|
for info in info_map:
|
||||||
|
data[info[0]] = info[1].strip()
|
||||||
|
|
||||||
|
if not attrs or 'description' in attrs:
|
||||||
|
desc = re.findall(r'\|\n+((\s+.+\n+)+)', full_info_lines)
|
||||||
|
data['description'] = ''.join([w.strip() for w in desc[0][0].strip().split('\n')]).replace('.', '.\n') if desc else None
|
||||||
|
|
||||||
|
if not attrs or 'commands' in attrs:
|
||||||
|
commands = re.findall(r'commands:\s*\n*((\s+-\s.+\s*\n)+)', full_info_lines)
|
||||||
|
data['commands'] = commands[0][0].replace('-', '').strip().split('\n') if commands else None
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def read_installed() -> List[dict]:
|
||||||
|
res = run_cmd('{} list'.format(BASE_CMD), print_error=False)
|
||||||
|
|
||||||
|
apps = []
|
||||||
|
|
||||||
|
if res and len(res) > 0:
|
||||||
|
lines = res.split('\n')
|
||||||
|
|
||||||
|
if not lines[0].startswith('error'):
|
||||||
|
for idx, app_str in enumerate(lines):
|
||||||
|
if idx > 0 and app_str:
|
||||||
|
apps.append(app_str_to_json(app_str))
|
||||||
|
|
||||||
|
return apps
|
||||||
|
|
||||||
|
|
||||||
|
def search(word: str, exact_name: bool = False) -> List[dict]:
|
||||||
|
apps = []
|
||||||
|
|
||||||
|
res = run_cmd('{} find "{}"'.format(BASE_CMD, word), print_error=False)
|
||||||
|
|
||||||
|
if res:
|
||||||
|
res = res.split('\n')
|
||||||
|
|
||||||
|
if not res[0].startswith('No matching'):
|
||||||
|
for idx, app_str in enumerate(res):
|
||||||
|
if idx > 0 and app_str:
|
||||||
|
app_data = [word for word in app_str.split(' ') if word]
|
||||||
|
|
||||||
|
if exact_name and app_data[0] != word:
|
||||||
|
continue
|
||||||
|
|
||||||
|
apps.append({
|
||||||
|
'name': app_data[0],
|
||||||
|
'version': app_data[1],
|
||||||
|
'publisher': app_data[2],
|
||||||
|
'notes': app_data[3] if app_data[3] != '-' else None,
|
||||||
|
'summary': app_data[4] if len(app_data) == 5 else '',
|
||||||
|
'rev': None,
|
||||||
|
'tracking': None,
|
||||||
|
'type': None
|
||||||
|
})
|
||||||
|
|
||||||
|
if exact_name and len(apps) > 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
return apps
|
||||||
|
|
||||||
|
|
||||||
|
def uninstall_and_stream(app_name: str, root_password: str):
|
||||||
|
return new_root_subprocess([BASE_CMD, 'remove', app_name], root_password)
|
||||||
|
|
||||||
|
|
||||||
|
def install_and_stream(app_name: str, confinement: str, root_password: str) -> subprocess.Popen:
|
||||||
|
|
||||||
|
install_cmd = [BASE_CMD, 'install', app_name] # default
|
||||||
|
|
||||||
|
if confinement == 'classic':
|
||||||
|
install_cmd.append('--classic')
|
||||||
|
|
||||||
|
return new_root_subprocess(install_cmd, root_password)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade_and_stream(app_name: str, root_password: str) -> subprocess.Popen:
|
||||||
|
return new_root_subprocess([BASE_CMD, 'revert', app_name], root_password)
|
||||||
|
|
||||||
|
|
||||||
|
def refresh_and_stream(app_name: str, root_password: str) -> subprocess.Popen:
|
||||||
|
return new_root_subprocess([BASE_CMD, 'refresh', app_name], root_password)
|
||||||
13
bauh/gems/snap/suggestions.py
Normal file
13
bauh/gems/snap/suggestions.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from bauh.api.abstract.model import SuggestionPriority
|
||||||
|
|
||||||
|
ALL = {
|
||||||
|
'whatsdesk': SuggestionPriority.TOP,
|
||||||
|
'slack': SuggestionPriority.HIGH,
|
||||||
|
'discord': SuggestionPriority.HIGH,
|
||||||
|
'yakyak': SuggestionPriority.MEDIUM,
|
||||||
|
'instagraph': SuggestionPriority.MEDIUM,
|
||||||
|
'gimp': SuggestionPriority.MEDIUM,
|
||||||
|
'pycharm-professional': SuggestionPriority.LOW,
|
||||||
|
'eclipse': SuggestionPriority.LOW,
|
||||||
|
'supertuxkart': SuggestionPriority.LOW
|
||||||
|
}
|
||||||
71
bauh/gems/snap/worker.py
Normal file
71
bauh/gems/snap/worker.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import traceback
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
from bauh.api.abstract.cache import MemoryCache
|
||||||
|
from bauh.api.abstract.context import ApplicationContext
|
||||||
|
from bauh.api.abstract.controller import SoftwareManager
|
||||||
|
from bauh.api.abstract.model import PackageStatus
|
||||||
|
|
||||||
|
from bauh.gems.snap import snap
|
||||||
|
from bauh.gems.snap.constants import SNAP_API_URL
|
||||||
|
from bauh.gems.snap.model import SnapApplication
|
||||||
|
|
||||||
|
|
||||||
|
class SnapAsyncDataLoader(Thread):
|
||||||
|
|
||||||
|
def __init__(self, app: SnapApplication, manager: SoftwareManager, api_cache: MemoryCache,
|
||||||
|
context: ApplicationContext):
|
||||||
|
super(SnapAsyncDataLoader, self).__init__(daemon=True)
|
||||||
|
self.app = app
|
||||||
|
self.id_ = '{}#{}'.format(self.__class__.__name__, id(self))
|
||||||
|
self.manager = manager
|
||||||
|
self.http_client = context.http_client
|
||||||
|
self.api_cache = api_cache
|
||||||
|
self.persist = False
|
||||||
|
self.download_icons = context.download_icons
|
||||||
|
self.logger = context.logger
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
if self.app:
|
||||||
|
self.app.status = PackageStatus.LOADING_DATA
|
||||||
|
|
||||||
|
try:
|
||||||
|
res = self.http_client.session.get('{}/search?q={}'.format(SNAP_API_URL, self.app.name))
|
||||||
|
|
||||||
|
if res:
|
||||||
|
try:
|
||||||
|
snap_list = res.json()['_embedded']['clickindex:package']
|
||||||
|
except:
|
||||||
|
self.logger.warning('Snap API response responded differently from expected for app: {}'.format(self.app.name))
|
||||||
|
return
|
||||||
|
|
||||||
|
if not snap_list:
|
||||||
|
self.logger.warning("Could not retrieve app data for id '{}'. Server response: {}. Body: {}".format(self.app.id, res.status_code, res.content.decode()))
|
||||||
|
else:
|
||||||
|
snap_data = snap_list[0]
|
||||||
|
|
||||||
|
api_data = {
|
||||||
|
'confinement': snap_data.get('confinement'),
|
||||||
|
'description': snap_data.get('description'),
|
||||||
|
'icon_url': snap_data.get('icon_url') if self.download_icons else None
|
||||||
|
}
|
||||||
|
|
||||||
|
self.api_cache.add(self.app.id, api_data)
|
||||||
|
self.app.confinement = api_data['confinement']
|
||||||
|
self.app.icon_url = api_data['icon_url']
|
||||||
|
|
||||||
|
if not api_data.get('description'):
|
||||||
|
api_data['description'] = snap.get_info(self.app.name, ('description',)).get('description')
|
||||||
|
|
||||||
|
self.app.description = api_data['description']
|
||||||
|
self.persist = self.app.supports_disk_cache()
|
||||||
|
else:
|
||||||
|
self.logger.warning("Could not retrieve app data for id '{}'. Server response: {}. Body: {}".format(self.app.id, res.status_code, res.content.decode()))
|
||||||
|
except:
|
||||||
|
self.logger.error("Could not retrieve app data for id '{}'".format(self.app.id))
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
self.app.status = PackageStatus.READY
|
||||||
|
|
||||||
|
if self.persist:
|
||||||
|
self.manager.cache_to_disk(pkg=self.app, icon_bytes=None, only_icon=False)
|
||||||
@@ -2,7 +2,7 @@ import datetime
|
|||||||
import time
|
import time
|
||||||
from threading import Lock, Thread
|
from threading import Lock, Thread
|
||||||
|
|
||||||
from bauh_api.abstract.cache import MemoryCache, MemoryCacheFactory
|
from bauh.api.abstract.cache import MemoryCache, MemoryCacheFactory
|
||||||
|
|
||||||
|
|
||||||
class DefaultMemoryCache(MemoryCache):
|
class DefaultMemoryCache(MemoryCache):
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import os
|
|||||||
from threading import Thread, Lock
|
from threading import Thread, Lock
|
||||||
from typing import Type, Dict
|
from typing import Type, Dict
|
||||||
|
|
||||||
from bauh_api.abstract.cache import MemoryCache
|
from bauh.api.abstract.cache import MemoryCache
|
||||||
from bauh_api.abstract.disk import DiskCacheLoader, DiskCacheLoaderFactory
|
from bauh.api.abstract.disk import DiskCacheLoader, DiskCacheLoaderFactory
|
||||||
from bauh_api.abstract.model import SoftwarePackage
|
from bauh.api.abstract.model import SoftwarePackage
|
||||||
|
|
||||||
|
|
||||||
class AsyncDiskCacheLoader(Thread, DiskCacheLoader):
|
class AsyncDiskCacheLoader(Thread, DiskCacheLoader):
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ from PyQt5.QtGui import QPixmap, QIcon, QCursor
|
|||||||
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest
|
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest
|
||||||
from PyQt5.QtWidgets import QTableWidget, QTableView, QMenu, QAction, QTableWidgetItem, QToolButton, QWidget, \
|
from PyQt5.QtWidgets import QTableWidget, QTableView, QMenu, QAction, QTableWidgetItem, QToolButton, QWidget, \
|
||||||
QHeaderView, QLabel, QHBoxLayout, QPushButton, QToolBar
|
QHeaderView, QLabel, QHBoxLayout, QPushButton, QToolBar
|
||||||
from bauh_api.abstract.cache import MemoryCache
|
from bauh.api.abstract.cache import MemoryCache
|
||||||
from bauh_api.abstract.model import PackageStatus
|
from bauh.api.abstract.model import PackageStatus
|
||||||
|
|
||||||
from bauh.util import resource
|
from bauh.util import resource
|
||||||
from bauh.util.html import strip_html
|
from bauh.util.html import strip_html
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
from PyQt5.QtCore import QEvent, Qt
|
from PyQt5.QtGui import QIcon
|
||||||
from PyQt5.QtWidgets import QRadioButton, QGroupBox, QCheckBox, QComboBox, QGridLayout, QWidget, \
|
from PyQt5.QtWidgets import QRadioButton, QGroupBox, QCheckBox, QComboBox, QGridLayout, QWidget, \
|
||||||
QLabel, QSizePolicy, QLineEdit
|
QLabel, QSizePolicy, QLineEdit
|
||||||
from bauh_api.abstract.view import SingleSelectComponent, InputOption, MultipleSelectComponent, SelectViewType
|
|
||||||
|
from bauh import ROOT_DIR
|
||||||
|
from bauh.api.abstract.view import SingleSelectComponent, InputOption, MultipleSelectComponent, SelectViewType
|
||||||
|
from bauh.commons import resource
|
||||||
|
|
||||||
|
|
||||||
class RadioButtonQt(QRadioButton):
|
class RadioButtonQt(QRadioButton):
|
||||||
@@ -19,19 +22,30 @@ class RadioButtonQt(QRadioButton):
|
|||||||
|
|
||||||
class CheckboxQt(QCheckBox):
|
class CheckboxQt(QCheckBox):
|
||||||
|
|
||||||
def __init__(self, model: InputOption, model_parent: MultipleSelectComponent):
|
def __init__(self, model: InputOption, model_parent: MultipleSelectComponent, callback):
|
||||||
super(CheckboxQt, self).__init__()
|
super(CheckboxQt, self).__init__()
|
||||||
self.model = model
|
self.model = model
|
||||||
self.model_parent = model_parent
|
self.model_parent = model_parent
|
||||||
self.stateChanged.connect(self._set_checked)
|
self.stateChanged.connect(self._set_checked)
|
||||||
|
self.callback = callback
|
||||||
|
self.setText(model.label)
|
||||||
|
self.setToolTip(model.tooltip)
|
||||||
|
|
||||||
|
if model.icon_path:
|
||||||
|
self.setIcon(QIcon(model.icon_path))
|
||||||
|
|
||||||
def _set_checked(self, state):
|
def _set_checked(self, state):
|
||||||
if state == 2:
|
checked = state == 2
|
||||||
|
|
||||||
|
if checked:
|
||||||
self.model_parent.values.add(self.model)
|
self.model_parent.values.add(self.model)
|
||||||
else:
|
else:
|
||||||
if self.model in self.model_parent.values:
|
if self.model in self.model_parent.values:
|
||||||
self.model_parent.values.remove(self.model)
|
self.model_parent.values.remove(self.model)
|
||||||
|
|
||||||
|
if self.callback:
|
||||||
|
self.callback(self.model, checked)
|
||||||
|
|
||||||
|
|
||||||
class ComboBoxQt(QComboBox):
|
class ComboBoxQt(QComboBox):
|
||||||
|
|
||||||
@@ -101,8 +115,8 @@ class ComboSelectQt(QGroupBox):
|
|||||||
|
|
||||||
class MultipleSelectQt(QGroupBox):
|
class MultipleSelectQt(QGroupBox):
|
||||||
|
|
||||||
def __init__(self, model: MultipleSelectComponent):
|
def __init__(self, model: MultipleSelectComponent, callback):
|
||||||
super(MultipleSelectQt, self).__init__(model.label + ' :' if model.label else None)
|
super(MultipleSelectQt, self).__init__(model.label if model.label else None)
|
||||||
self.setStyleSheet("QGroupBox { font-weight: bold }")
|
self.setStyleSheet("QGroupBox { font-weight: bold }")
|
||||||
self.model = model
|
self.model = model
|
||||||
self._layout = QGridLayout()
|
self._layout = QGridLayout()
|
||||||
@@ -110,9 +124,7 @@ class MultipleSelectQt(QGroupBox):
|
|||||||
|
|
||||||
line, col = 0, 0
|
line, col = 0, 0
|
||||||
for op in model.options:
|
for op in model.options:
|
||||||
comp = CheckboxQt(op, model)
|
comp = CheckboxQt(op, model, callback)
|
||||||
comp.setText(op.label)
|
|
||||||
comp.setToolTip(op.tooltip)
|
|
||||||
|
|
||||||
if model.values and op in model.values:
|
if model.values and op in model.values:
|
||||||
self.value = comp
|
self.value = comp
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from PyQt5.QtWidgets import QMessageBox, QVBoxLayout, QLabel, QWidget
|
from PyQt5.QtWidgets import QMessageBox, QVBoxLayout, QLabel, QWidget
|
||||||
from bauh_api.abstract.view import ViewComponent, SingleSelectComponent, MultipleSelectComponent
|
from bauh.api.abstract.view import ViewComponent, SingleSelectComponent, MultipleSelectComponent
|
||||||
|
|
||||||
from bauh.view.qt.components import MultipleSelectQt, new_single_select
|
from bauh.view.qt.components import MultipleSelectQt, new_single_select
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ class ConfirmationDialog(QMessageBox):
|
|||||||
if isinstance(comp, SingleSelectComponent):
|
if isinstance(comp, SingleSelectComponent):
|
||||||
inst = new_single_select(comp)
|
inst = new_single_select(comp)
|
||||||
elif isinstance(comp, MultipleSelectComponent):
|
elif isinstance(comp, MultipleSelectComponent):
|
||||||
inst = MultipleSelectQt(comp)
|
inst = MultipleSelectQt(comp, None)
|
||||||
else:
|
else:
|
||||||
raise Exception("Cannot render instances of " + comp.__class__.__name__)
|
raise Exception("Cannot render instances of " + comp.__class__.__name__)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from typing import List
|
|||||||
|
|
||||||
from PyQt5.QtGui import QIcon
|
from PyQt5.QtGui import QIcon
|
||||||
from PyQt5.QtWidgets import QMessageBox, QLabel, QWidget, QHBoxLayout
|
from PyQt5.QtWidgets import QMessageBox, QLabel, QWidget, QHBoxLayout
|
||||||
from bauh_api.abstract.view import MessageType
|
from bauh.api.abstract.view import MessageType
|
||||||
|
|
||||||
from bauh.util import resource
|
from bauh.util import resource
|
||||||
|
|
||||||
|
|||||||
69
bauh/view/qt/gem_selector.py
Normal file
69
bauh/view/qt/gem_selector.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from PyQt5.QtCore import Qt, QCoreApplication
|
||||||
|
from PyQt5.QtGui import QIcon
|
||||||
|
from PyQt5.QtWidgets import QWidget, QLabel, QGridLayout, QPushButton
|
||||||
|
|
||||||
|
from bauh import ROOT_DIR
|
||||||
|
from bauh.api.abstract.controller import SoftwareManager
|
||||||
|
from bauh.api.abstract.view import MultipleSelectComponent, InputOption
|
||||||
|
from bauh.commons import resource
|
||||||
|
from bauh.core.config import Configuration, save
|
||||||
|
from bauh.view.qt.components import MultipleSelectQt, CheckboxQt, new_spacer
|
||||||
|
|
||||||
|
|
||||||
|
class GemSelectorPanel(QWidget):
|
||||||
|
|
||||||
|
def __init__(self, managers: List[SoftwareManager], i18n: dict, boot: bool):
|
||||||
|
super(GemSelectorPanel, self).__init__()
|
||||||
|
self.managers = managers
|
||||||
|
self.setLayout(QGridLayout())
|
||||||
|
self.setWindowIcon(QIcon(resource.get_path('img/logo.svg', ROOT_DIR)))
|
||||||
|
self.setWindowTitle('Welcome' if boot else 'Supported types')
|
||||||
|
self.resize(400, 400)
|
||||||
|
|
||||||
|
self.label_question = QLabel('What types of applications do you want to find here ?')
|
||||||
|
self.label_question.setStyleSheet('QLabel { font-weight: bold}')
|
||||||
|
self.layout().addWidget(self.label_question, 0, 1, Qt.AlignHCenter)
|
||||||
|
|
||||||
|
self.bt_proceed = QPushButton('Proceed')
|
||||||
|
self.bt_proceed.setStyleSheet('QPushButton { background: green; color: white; font-weight: bold}')
|
||||||
|
self.bt_proceed.setEnabled(True)
|
||||||
|
self.bt_proceed.clicked.connect(self.save)
|
||||||
|
|
||||||
|
self.bt_exit = QPushButton('Exit')
|
||||||
|
self.bt_exit.setStyleSheet('QPushButton { background: red; color: white; font-weight: bold}')
|
||||||
|
self.bt_exit.clicked.connect(lambda: QCoreApplication.exit())
|
||||||
|
|
||||||
|
gem_options = []
|
||||||
|
|
||||||
|
for m in managers:
|
||||||
|
modname = m.__module__.split('.')[-2]
|
||||||
|
gem_options.append(InputOption(label=i18n.get('gem.{}.label'.format(modname), modname.capitalize()),
|
||||||
|
value=modname,
|
||||||
|
icon_path='{r}/gems/{n}/resources/img/{n}.png'.format(r=ROOT_DIR, n=modname)))
|
||||||
|
|
||||||
|
self.gem_select_model = MultipleSelectComponent(label='', options=gem_options, default_options=set(gem_options), max_per_line=3)
|
||||||
|
|
||||||
|
self.gem_select = MultipleSelectQt(self.gem_select_model, self.check_state)
|
||||||
|
self.layout().addWidget(self.gem_select, 1, 1)
|
||||||
|
|
||||||
|
self.layout().addWidget(new_spacer(), 2, 1)
|
||||||
|
|
||||||
|
self.layout().addWidget(self.bt_proceed, 3, 1, Qt.AlignRight)
|
||||||
|
self.layout().addWidget(self.bt_exit, 3, 1, Qt.AlignLeft)
|
||||||
|
|
||||||
|
self.adjustSize()
|
||||||
|
self.setFixedSize(self.size())
|
||||||
|
|
||||||
|
def check_state(self, model: CheckboxQt, checked: bool):
|
||||||
|
self.bt_proceed.setEnabled(bool(self.gem_select_model.values))
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
config = Configuration(gems=[o.value for o in self.gem_select_model.values])
|
||||||
|
save(config)
|
||||||
|
subprocess.Popen([sys.executable, *sys.argv])
|
||||||
|
self.close()
|
||||||
|
|
||||||
@@ -4,8 +4,8 @@ from functools import reduce
|
|||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
from PyQt5.QtGui import QColor
|
from PyQt5.QtGui import QColor
|
||||||
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QTableWidget, QTableWidgetItem, QHeaderView
|
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QTableWidget, QTableWidgetItem, QHeaderView
|
||||||
from bauh_api.abstract.cache import MemoryCache
|
from bauh.api.abstract.cache import MemoryCache
|
||||||
from bauh_api.abstract.model import PackageHistory
|
from bauh.api.abstract.model import PackageHistory
|
||||||
|
|
||||||
|
|
||||||
class HistoryDialog(QDialog):
|
class HistoryDialog(QDialog):
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from PyQt5.QtCore import QSize
|
|||||||
from PyQt5.QtGui import QIcon
|
from PyQt5.QtGui import QIcon
|
||||||
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QGroupBox, \
|
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QGroupBox, \
|
||||||
QLineEdit, QLabel, QGridLayout, QPushButton, QPlainTextEdit, QToolBar
|
QLineEdit, QLabel, QGridLayout, QPushButton, QPlainTextEdit, QToolBar
|
||||||
from bauh_api.abstract.cache import MemoryCache
|
from bauh.api.abstract.cache import MemoryCache
|
||||||
|
|
||||||
from bauh.util.html import strip_html
|
from bauh.util.html import strip_html
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import os
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from PyQt5.QtWidgets import QInputDialog, QLineEdit
|
from PyQt5.QtWidgets import QInputDialog, QLineEdit
|
||||||
from bauh_api.abstract.view import MessageType
|
from bauh.api.abstract.view import MessageType
|
||||||
|
|
||||||
from bauh.view.qt.dialog import show_message
|
from bauh.view.qt.dialog import show_message
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ from typing import List
|
|||||||
from PyQt5.QtCore import QThread, pyqtSignal, QCoreApplication, Qt
|
from PyQt5.QtCore import QThread, pyqtSignal, QCoreApplication, Qt
|
||||||
from PyQt5.QtGui import QIcon
|
from PyQt5.QtGui import QIcon
|
||||||
from PyQt5.QtWidgets import QSystemTrayIcon, QMenu
|
from PyQt5.QtWidgets import QSystemTrayIcon, QMenu
|
||||||
from bauh_api.abstract.model import PackageUpdate
|
from bauh.api.abstract.model import PackageUpdate
|
||||||
|
|
||||||
from bauh import __app_name__
|
from bauh import __app_name__
|
||||||
from bauh_api.abstract.controller import SoftwareManager
|
from bauh.api.abstract.controller import SoftwareManager
|
||||||
from bauh.util import util, resource
|
from bauh.util import util, resource
|
||||||
from bauh.view.qt.about import AboutDialog
|
from bauh.view.qt.about import AboutDialog
|
||||||
from bauh.view.qt.window import ManageWindow
|
from bauh.view.qt.window import ManageWindow
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ from typing import List, Type, Set
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
from PyQt5.QtCore import QThread, pyqtSignal
|
from PyQt5.QtCore import QThread, pyqtSignal
|
||||||
from bauh_api.abstract.cache import MemoryCache
|
from bauh.api.abstract.cache import MemoryCache
|
||||||
from bauh_api.abstract.controller import SoftwareManager
|
from bauh.api.abstract.controller import SoftwareManager
|
||||||
from bauh_api.abstract.handler import ProcessWatcher
|
from bauh.api.abstract.handler import ProcessWatcher
|
||||||
from bauh_api.abstract.model import PackageStatus, SoftwarePackage, PackageAction
|
from bauh.api.abstract.model import PackageStatus, SoftwarePackage, PackageAction
|
||||||
from bauh_api.abstract.view import InputViewComponent, MessageType
|
from bauh.api.abstract.view import InputViewComponent, MessageType
|
||||||
from bauh_api.exception import NoInternetException
|
from bauh.api.exception import NoInternetException
|
||||||
|
|
||||||
from bauh.view.qt import commons
|
from bauh.view.qt import commons
|
||||||
from bauh.view.qt.view_model import PackageView
|
from bauh.view.qt.view_model import PackageView
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from bauh_api.abstract.model import SoftwarePackage
|
from bauh.api.abstract.model import SoftwarePackage
|
||||||
|
|
||||||
|
|
||||||
class PackageViewStatus(Enum):
|
class PackageViewStatus(Enum):
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ from PyQt5.QtCore import QEvent, Qt, QSize, pyqtSignal
|
|||||||
from PyQt5.QtGui import QIcon, QWindowStateChangeEvent, QPixmap
|
from PyQt5.QtGui import QIcon, QWindowStateChangeEvent, QPixmap
|
||||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QCheckBox, QHeaderView, QToolButton, QToolBar, \
|
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QCheckBox, QHeaderView, QToolButton, QToolBar, \
|
||||||
QLabel, QPlainTextEdit, QLineEdit, QProgressBar, QPushButton, QComboBox
|
QLabel, QPlainTextEdit, QLineEdit, QProgressBar, QPushButton, QComboBox
|
||||||
from bauh_api.abstract.cache import MemoryCache
|
from bauh.api.abstract.cache import MemoryCache
|
||||||
from bauh_api.abstract.controller import SoftwareManager
|
from bauh.api.abstract.controller import SoftwareManager
|
||||||
from bauh_api.abstract.model import SoftwarePackage, PackageAction
|
from bauh.api.abstract.model import SoftwarePackage, PackageAction
|
||||||
from bauh_api.abstract.view import MessageType
|
from bauh.api.abstract.view import MessageType
|
||||||
|
|
||||||
from bauh.util import util, resource
|
from bauh.util import util, resource
|
||||||
from bauh.view.qt import dialog, commons
|
from bauh.view.qt import dialog, commons
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
pyqt5>=5.12
|
pyqt5>=5.12
|
||||||
bauh_api>=0.1.0<0.2
|
requests>=2.22
|
||||||
|
colorama>=0.4.1
|
||||||
|
|||||||
Reference in New Issue
Block a user