From 53bf6de34f1f361ed0b961e3d17f6456b9f71092 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Thu, 3 Oct 2019 19:09:31 -0300 Subject: [PATCH] installing appimage --- .gitignore | 1 + bauh/api/abstract/model.py | 3 +- bauh/app.py | 6 +- bauh/gems/appimage/__init__.py | 6 + bauh/gems/appimage/controller.py | 129 ++++++++++++++++++ bauh/gems/appimage/model.py | 60 ++++++++ bauh/gems/appimage/query.py | 5 + bauh/gems/appimage/resources/img/appimage.png | Bin 0 -> 1627 bytes bauh/gems/appimage/resources/locale/en | 2 + bauh/gems/appimage/resources/locale/es | 2 + bauh/gems/appimage/resources/locale/pt | 2 + bauh/gems/arch/controller.py | 4 - bauh/view/core/downloader.py | 16 ++- bauh/view/qt/apps_table.py | 5 +- 14 files changed, 230 insertions(+), 11 deletions(-) create mode 100644 bauh/gems/appimage/__init__.py create mode 100644 bauh/gems/appimage/controller.py create mode 100644 bauh/gems/appimage/model.py create mode 100644 bauh/gems/appimage/query.py create mode 100755 bauh/gems/appimage/resources/img/appimage.png create mode 100644 bauh/gems/appimage/resources/locale/en create mode 100644 bauh/gems/appimage/resources/locale/es create mode 100644 bauh/gems/appimage/resources/locale/pt 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 0000000000000000000000000000000000000000..059771a7cc078e4a3ba2a6cd0df8c66446d789ac GIT binary patch literal 1627 zcmV-h2Bi6kP)EX>4Tx04R}tkv&MmKpe$iQ$;BihjtKg$WX<>f~bh2R-p(LLaorMgUO{|(4-+r zad8w}3l4rPRvlcNb#-tR1i=pw7gr}m7b)?7NufoI2gm(*ckglc4iFj@rdeI%fTr7K zI++l&xmB^}6(Ix>K^UVlvy3@OO2K!0-6O!)yExDCKlkV8Q}Y%B0wVD&GfbO!gLrz= zHaPDSM_EZ$iO-40Ou8WPBi9v=-#8at7Id3pDGF^L^|%%@ZK_3|#4Lf3*S3e3D*o zYq2Ase;c^CZfnXOaJd5vJQ=bnyHb#*P$&TJXY@@uAaV6&ffk#)9UXBIbw244JLiUl3D-&1XxK#K~zY`t(MDcT~!pue`}v} zlbhToX=0k@rcJ4~;G;xGRwxmjNf8koh(p1VgJMAkI#3V?A~+Bo_y_1f#6cB=qAds( zqLda&Vxvf(Ay%4a)8zJ?d-h(-!FlA~l+HYGS(|g${=W6C$Jtu|PJI03$%B*oK8>O% z5)ni|01>GyKw&T|tzlg@UxyIF^@YWEzIgA=&tiZBllxBY8f`|Y_e9R&oDhjrYcWNA zRuP#QF0NtXP&I-o!K_;M#c^~1-U4z0C(_?)XCzr5b~$hq71jwMO_-V-Wny#~fPa?P zm|a*V>a7z>f{1eT`SiJbQ!9MH)Cgwa116H^p1cl_`>84bE?1yMfr^3^)_ac&ZJGmhdTMd;pV3o` zX%5tJKE-?AwWq`hI|lf5egCz4zPaR@>WUwL_9~oqN>Jay`fEMYve}5-IP=YC{ zWhdlLhTvVy4>NPLlC&adB`M#}%%M>YL$(7d_nQ*JP6fIhdEOP8M6=Co20PSnH^C2@sq6jNUv?9ncs}%X5_+XWa5b_Ax zS%$U~_7C=uCMky}_VDE4Niy$=HDh8|OqwKl20HfputR|_a~KqJf=aF+OH*Edeu{(5 zQRc2LGO=ej8Un8#JHpuTAdS9WKKkY?J@u%Vhg^jem>mj2Rfjwp#Ro-!AAg@G%RG-w zj^W4$=;84Zh8lHdX0M<)f?2hv#PCImDh0mi0w;osp}vi8w^{O0{uMRh?1evB-Ap)s z^a&awY$hpbJ0U>r&tYJ{W3>6#4gR{Bd=WvEcz8nHXhgS0AE* zG!5)-^r5P(Y;3W*u|=!xky>2U!I{JR(5WELnHGJoBH&`ayKoito)2Gpj{e>nZ@+wu zJDV*g_l^K?>f9Wcm+s=C7-wLr1x6sdugJw5crA)md}CgE>L87J4_=K%y~f1o5Sb5{ z8804v6rXt%*HOn3qhk&-ABxK7^N>5_J-x8B&XvU_-v8`$xaS>asgm&gxyqj?9(oIyAxTlUU&WiF}C^0+R zRLi#NJm>`=t|8RW5cvG3OF(`eySN7df`J9?x&V}z0CBU~Y^>k9aWU#2I_{#V>jqI! zKyk|`Pd&URP3}Q?yTH0`8X5xYw{Bc)Hk*yu%#zhBvv0K4ZcmHIApgT-W*f=s?Q>?9 Z{0ALe$-d#$c{Kn4002ovPDHLkV1j??-&g 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)