diff --git a/.gitignore b/.gitignore index 02d0143a..9ef8421a 100755 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ __pycache__ dist *.egg-info build +*.db \ No newline at end of file diff --git a/bauh/api/abstract/model.py b/bauh/api/abstract/model.py index c0bf8379..188f87cb 100644 --- a/bauh/api/abstract/model.py +++ b/bauh/api/abstract/model.py @@ -32,7 +32,7 @@ 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): + size: int = None, categories: List[str] = None, license: str = None): """ :param id: :param version: @@ -57,6 +57,7 @@ class SoftwarePackage(ABC): self.update = update self.size = size self.categories = categories + self.license = license @abstractmethod def has_history(self): diff --git a/bauh/app.py b/bauh/app.py index bab13b2a..bb04c655 100755 --- a/bauh/app.py +++ b/bauh/app.py @@ -31,15 +31,17 @@ def main(): cache_factory = DefaultMemoryCacheFactory(expiration_time=args.cache_exp, cleaner=cache_cleaner) icon_cache = cache_factory.new(args.icon_exp) + http_client = HttpClient(logger) context = ApplicationContext(i18n=i18n, - http_client=HttpClient(logger), + http_client=http_client, disk_cache=args.disk_cache, download_icons=args.download_icons, app_root_dir=ROOT_DIR, cache_factory=cache_factory, disk_loader_factory=DefaultDiskCacheLoaderFactory(disk_cache_enabled=args.disk_cache, logger=logger), logger=logger, - file_downloader=AdaptableFileDownloader(logger, bool(args.download_mthread))) + file_downloader=AdaptableFileDownloader(logger, bool(args.download_mthread), + i18n, http_client)) user_config = config.read() app = QApplication(sys.argv) diff --git a/bauh/gems/appimage/__init__.py b/bauh/gems/appimage/__init__.py new file mode 100644 index 00000000..25301604 --- /dev/null +++ b/bauh/gems/appimage/__init__.py @@ -0,0 +1,6 @@ +import os + +from bauh.api.constants import HOME_PATH + +ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) +INSTALLATION_PATH = '{}/.local/share/bauh/appimage/installed/'.format(HOME_PATH) diff --git a/bauh/gems/appimage/controller.py b/bauh/gems/appimage/controller.py new file mode 100644 index 00000000..56edf62b --- /dev/null +++ b/bauh/gems/appimage/controller.py @@ -0,0 +1,129 @@ +import os +import sqlite3 +from pathlib import Path +from typing import Set, Type, List + +from bauh.api.abstract.context import ApplicationContext +from bauh.api.abstract.controller import SoftwareManager, SearchResult +from bauh.api.abstract.disk import DiskCacheLoader +from bauh.api.abstract.handler import ProcessWatcher +from bauh.api.abstract.model import SoftwarePackage, PackageHistory, PackageUpdate, PackageSuggestion +from bauh.api.constants import HOME_PATH +from bauh.commons.html import bold +from bauh.commons.system import SystemProcess, new_subprocess, ProcessHandler +from bauh.gems.appimage import query, INSTALLATION_PATH +from bauh.gems.appimage.model import AppImage + +DB_PATH = '{}/{}'.format(HOME_PATH, '.cache/bauh/appimage/appimage.db') + + +class AppImageManager(SoftwareManager): + + def __init__(self, context: ApplicationContext): + super(AppImageManager, self).__init__(context=context) + self.i18n = context.i18n + self.api_cache = context.cache_factory.new() + context.disk_loader_factory.map(AppImageManager, self.api_cache) + self.enabled = True + self.http_client = context.http_client + self.logger = context.logger + self.file_downloader = context.file_downloader + + def _get_db_connection(self) -> sqlite3.Connection: + if os.path.exists(DB_PATH): + return sqlite3.connect(DB_PATH) + + def search(self, words: str, disk_loader: DiskCacheLoader, limit: int = -1) -> SearchResult: + res = SearchResult([], [], 0) + connection = self._get_db_connection() + + if connection: + try: + cursor = connection.cursor() + cursor.execute(query.SEARCH_BY_NAME_OR_DESCRIPTION.format(words, words)) + + for l in cursor.fetchall(): + app = AppImage(*l) + res.new.append(app) + finally: + connection.close() + + 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, internet_available: bool = None) -> SearchResult: + return SearchResult([], [], 0) + + def downgrade(self, pkg: AppImage, root_password: str, watcher: ProcessWatcher) -> bool: + pass + + def update(self, pkg: AppImage, root_password: str, watcher: ProcessWatcher) -> SystemProcess: + pass + + def uninstall(self, pkg: AppImage, root_password: str, watcher: ProcessWatcher) -> bool: + pass + + def get_managed_types(self) -> Set[Type[SoftwarePackage]]: + return {AppImage} + + def clean_cache_for(self, pkg: AppImage): + # TODO + pass + + def get_info(self, pkg: AppImage) -> dict: + # TODO + pass + + def get_history(self, pkg: AppImage) -> PackageHistory: + # TODO + pass + + def install(self, pkg: AppImage, root_password: str, watcher: ProcessWatcher) -> bool: + out_dir = INSTALLATION_PATH + pkg.name.lower() + Path(out_dir).mkdir(parents=True, exist_ok=True) + + file_name = pkg.url_download.split('/')[-1] + file_path = out_dir + '/' + file_name + downloaded = self.file_downloader.download(pkg.url_download, watcher=watcher, output_path=file_path, cwd=HOME_PATH) + + if downloaded: + handler = ProcessHandler(watcher) + watcher.change_substatus(self.i18n['appimage.install.permission'].format(bold(file_name))) + return handler.handle(SystemProcess(new_subprocess(['chmod', 'a+x', file_path]))) + + return False + + def is_enabled(self) -> bool: + return self.enabled + + def set_enabled(self, enabled: bool): + self.enabled = enabled + + def can_work(self) -> bool: + # TODO check wget and sqlite + return True + + def requires_root(self, action: str, pkg: AppImage): + return False + + def prepare(self): + # TODO + pass + + def list_updates(self, internet_available: bool) -> List[PackageUpdate]: + # TODO + pass + + def list_warnings(self) -> List[str]: + pass + + def list_suggestions(self, limit: int) -> List[PackageSuggestion]: + # TODO + pass + + def is_default_enabled(self) -> bool: + return True + + def launch(self, pkg: AppImage): + # TODO + pass diff --git a/bauh/gems/appimage/model.py b/bauh/gems/appimage/model.py new file mode 100644 index 00000000..ad99531c --- /dev/null +++ b/bauh/gems/appimage/model.py @@ -0,0 +1,60 @@ +from typing import List + +from bauh.api.abstract.model import SoftwarePackage +from bauh.commons import resource +from bauh.gems.appimage import ROOT_DIR + + +class AppImage(SoftwarePackage): + + def __init__(self, name: str = None, description: str = None, github: str = None, source: str = None, version: str = None, + url_download: str = None, url_icon: str = None, license: str = None, pictures: List[str] = None): + super(AppImage, self).__init__(name=name, version=version, latest_version=version, + icon_url=url_icon, license=license, description=description) + self.source = source + self.github = github + self.pictures = pictures + self.url_download = url_download + + def can_be_installed(self): + return not self.installed and self.url_download + + def has_history(self): + # TODO + return False + + def has_info(self): + # TODO + return False + + def can_be_downgraded(self): + # TODO + return False + + def get_type(self): + return 'AppImage' + + def get_default_icon_path(self): + return self.get_type_icon_path() + + def get_type_icon_path(self): + return resource.get_path('img/appimage.png', ROOT_DIR) + + def is_application(self): + return True + + def get_data_to_cache(self) -> dict: + # TODO + pass + + def fill_cached_data(self, data: dict): + # TODO + pass + + def can_be_run(self) -> str: + # TODO + return False + + def get_publisher(self) -> str: + # TODO + pass diff --git a/bauh/gems/appimage/query.py b/bauh/gems/appimage/query.py new file mode 100644 index 00000000..e025465a --- /dev/null +++ b/bauh/gems/appimage/query.py @@ -0,0 +1,5 @@ +ATTRS = ('name', 'description', 'github', 'source', 'version', 'url_download', 'url_icon', 'license') + +_SELECT_BASE = "SELECT {} FROM apps".format(','.join(ATTRS)) + +SEARCH_BY_NAME_OR_DESCRIPTION = _SELECT_BASE + " WHERE lower(name) LIKE '%{}%' or lower(description) LIKE '%{}%'" diff --git a/bauh/gems/appimage/resources/img/appimage.png b/bauh/gems/appimage/resources/img/appimage.png new file mode 100755 index 00000000..059771a7 Binary files /dev/null and b/bauh/gems/appimage/resources/img/appimage.png differ diff --git a/bauh/gems/appimage/resources/locale/en b/bauh/gems/appimage/resources/locale/en new file mode 100644 index 00000000..dbdf2fc1 --- /dev/null +++ b/bauh/gems/appimage/resources/locale/en @@ -0,0 +1,2 @@ +gem.appimage.label=AppImage +appimage.install.permission=Giving execution permission to {} \ No newline at end of file diff --git a/bauh/gems/appimage/resources/locale/es b/bauh/gems/appimage/resources/locale/es new file mode 100644 index 00000000..3e633ebf --- /dev/null +++ b/bauh/gems/appimage/resources/locale/es @@ -0,0 +1,2 @@ +gem.appimage.label=AppImage +appimage.install.permission=Concediendo permiso de ejecución a {} \ No newline at end of file diff --git a/bauh/gems/appimage/resources/locale/pt b/bauh/gems/appimage/resources/locale/pt new file mode 100644 index 00000000..2d0be201 --- /dev/null +++ b/bauh/gems/appimage/resources/locale/pt @@ -0,0 +1,2 @@ +gem.appimage.label=AppImage +appimage.install.permission=Concedendo permissão de execução para {} \ No newline at end of file diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index 9c55b258..5de9f02c 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -405,10 +405,6 @@ class ArchManager(SoftwareManager): else: args.update({'file_url': fdata[0], 'output_path': None}) - file_size = self.context.http_client.get_content_length(args['file_url']) - file_size = int(file_size) / (1024 ** 2) if file_size else None - - watcher.change_substatus(bold('[{}] ').format(downloader) + self.i18n['downloading'] + ' ' + bold(args['file_url'].split('/')[-1]) + ' ( {0:.2f} Mb )'.format(file_size) if file_size else '') if not self.context.file_downloader.download(**args): watcher.print('Could not download source file {}'.format(args['file_url'])) return False diff --git a/bauh/view/core/downloader.py b/bauh/view/core/downloader.py index 971160b0..a95471f0 100644 --- a/bauh/view/core/downloader.py +++ b/bauh/view/core/downloader.py @@ -5,14 +5,18 @@ import traceback from bauh.api.abstract.download import FileDownloader from bauh.api.abstract.handler import ProcessWatcher +from bauh.api.http import HttpClient +from bauh.commons.html import bold from bauh.commons.system import run_cmd, new_subprocess, ProcessHandler, SystemProcess class AdaptableFileDownloader(FileDownloader): - def __init__(self, logger: logging.Logger, multithread_enabled: bool): + def __init__(self, logger: logging.Logger, multithread_enabled: bool, i18n: dict, http_client: HttpClient): self.logger = logger self.multithread_enabled = multithread_enabled + self.i18n = i18n + self.http_client = http_client def is_aria2c_available(self) -> bool: return bool(run_cmd('which aria2c')) @@ -48,7 +52,7 @@ class AdaptableFileDownloader(FileDownloader): cmd = ['wget', url] if output_path: - cmd.append('-O') + cmd.append('-o') cmd.append(output_path) return SystemProcess(new_subprocess(cmd, cwd=cwd)) @@ -73,10 +77,16 @@ class AdaptableFileDownloader(FileDownloader): if self.is_multithreaded(): ti = time.time() process = self._get_aria2c_process(file_url, output_path, final_cwd) + downloader = 'aria2c' else: ti = time.time() process = self._get_wget_process(file_url, output_path, final_cwd) + downloader = 'wget' + file_size = self.http_client.get_content_length(file_url) + file_size = int(file_size) / (1024 ** 2) if file_size else None + msg = bold('[{}] ').format(downloader) + self.i18n['downloading'] + ' ' + bold(file_url.split('/')[-1]) + (' ( {0:.2f} Mb )'.format(file_size) if file_size else '') + watcher.change_substatus(msg) success = handler.handle(process) except: traceback.print_exc() @@ -91,6 +101,8 @@ class AdaptableFileDownloader(FileDownloader): return success + + def is_multithreaded(self) -> bool: return self.multithread_enabled and self.is_aria2c_available() diff --git a/bauh/view/qt/apps_table.py b/bauh/view/qt/apps_table.py index 309620d1..966c5d51 100644 --- a/bauh/view/qt/apps_table.py +++ b/bauh/view/qt/apps_table.py @@ -4,7 +4,7 @@ from typing import List from PyQt5.QtCore import Qt, QUrl from PyQt5.QtGui import QPixmap, QIcon, QCursor -from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest +from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply from PyQt5.QtWidgets import QTableWidget, QTableView, QMenu, QAction, QTableWidgetItem, QToolButton, QWidget, \ QHeaderView, QLabel, QHBoxLayout, QPushButton, QToolBar @@ -175,7 +175,8 @@ class AppsTable(QTableWidget): self.window.install(pkgv) - def _load_icon_and_cache(self, http_response): + def _load_icon_and_cache(self, http_response: QNetworkReply): + icon_url = http_response.url().toString() icon_data = self.icon_cache.get(icon_url)