diff --git a/bauh/api/constants.py b/bauh/api/constants.py index 2eceab3a..a14e0b8d 100644 --- a/bauh/api/constants.py +++ b/bauh/api/constants.py @@ -3,3 +3,4 @@ from pathlib import Path HOME_PATH = Path.home() CACHE_PATH = '{}/.cache/bauh'.format(HOME_PATH) CONFIG_PATH = '{}/.config/bauh'.format(HOME_PATH) +DESKTOP_ENTRIES_DIR = '{}/.local/share/applications'.format(HOME_PATH) diff --git a/bauh/gems/web/__init__.py b/bauh/gems/web/__init__.py index 3d756e18..c0f8819b 100644 --- a/bauh/gems/web/__init__.py +++ b/bauh/gems/web/__init__.py @@ -1,9 +1,10 @@ import os -from bauh.api.constants import HOME_PATH +from bauh.api.constants import HOME_PATH, DESKTOP_ENTRIES_DIR ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) BIN_PATH = '{}/.local/share/bauh/web'.format(HOME_PATH) +INSTALLED_PATH = '{}/installed'.format(BIN_PATH) NODE_DIR_PATH = '{}/node'.format(BIN_PATH) NODE_BIN_PATH = '{}/bin/node'.format(NODE_DIR_PATH) NPM_BIN_PATH = '{}/bin/npm'.format(NODE_DIR_PATH) @@ -13,3 +14,4 @@ ELECTRON_PATH = '{}/.cache/electron'.format(HOME_PATH) ELECTRON_DOWNLOAD_URL = 'https://github.com/electron/electron/releases/download/v{version}/electron-v{version}-linux-{arch}.zip' ELECTRON_SHA256_URL = 'https://github.com/electron/electron/releases/download/v{version}/SHASUMS256.txt' URL_ENVIRONMENT_SETTINGS = 'https://raw.githubusercontent.com/vinifmor/bauh-files/master/web/environment.yml' +DESKTOP_ENTRY_PATH_PATTERN = DESKTOP_ENTRIES_DIR + '/bauh.web.{name}.desktop' diff --git a/bauh/gems/web/controller.py b/bauh/gems/web/controller.py index 159feb11..e2b75b8c 100644 --- a/bauh/gems/web/controller.py +++ b/bauh/gems/web/controller.py @@ -1,4 +1,10 @@ +import glob +import json import os +import shutil +import subprocess +import traceback +from pathlib import Path from threading import Thread from typing import List, Type, Set @@ -8,8 +14,10 @@ from bauh.api.abstract.disk import DiskCacheLoader from bauh.api.abstract.handler import ProcessWatcher from bauh.api.abstract.model import SoftwarePackage, PackageAction, PackageSuggestion, PackageUpdate, PackageHistory from bauh.api.abstract.view import MessageType +from bauh.commons import system from bauh.commons.html import bold from bauh.commons.system import ProcessHandler +from bauh.gems.web import INSTALLED_PATH, nativefier, DESKTOP_ENTRY_PATH_PATTERN from bauh.gems.web.environment import EnvironmentUpdater from bauh.gems.web.model import WebApplication @@ -37,6 +45,8 @@ class WebApplicationManager(SoftwareManager): self.enabled = True self.i18n = context.i18n self.env_updater = env_updater + self.env_settings = {} + self.logger = context.logger def search(self, words: str, disk_loader: DiskCacheLoader, limit: int = -1, is_url: bool = False) -> SearchResult: res = SearchResult([], [], 0) @@ -56,7 +66,13 @@ class WebApplicationManager(SoftwareManager): icon_tag = soup.head.find('link', attrs={"rel": "icon"}) icon_url = icon_tag.get('href') if icon_tag else None - res.new = [WebApplication(url=words, name=name, description=desc, icon_url=icon_url)] + app = WebApplication(url=words, name=name, description=desc, icon_url=icon_url) + + if self.env_settings.get('electron') and self.env_settings['electron'].get('version'): + app.version = self.env_settings['electron']['version'] + app.latest_version = app.version + + res.new = [app] res.total = 1 else: # TODO @@ -65,8 +81,15 @@ class WebApplicationManager(SoftwareManager): return res def read_installed(self, disk_loader: DiskCacheLoader, limit: int = -1, only_apps: bool = False, pkg_types: Set[Type[SoftwarePackage]] = None, internet_available: bool = True) -> SearchResult: - # TODO - return SearchResult([], [], 0) + res = SearchResult([], [], 0) + + if os.path.exists(INSTALLED_PATH): + for data_path in glob.glob('{}/*/*data.json'.format(INSTALLED_PATH)): + with open(data_path, 'r') as f: + res.installed.append(WebApplication(installed=True, **json.loads(f.read()))) + res.total += 1 + + return res def downgrade(self, pkg: SoftwarePackage, root_password: str, handler: ProcessWatcher) -> bool: pass @@ -74,14 +97,47 @@ class WebApplicationManager(SoftwareManager): def update(self, pkg: SoftwarePackage, root_password: str, watcher: ProcessWatcher) -> bool: pass - def uninstall(self, pkg: SoftwarePackage, root_password: str, watcher: ProcessWatcher) -> bool: - pass + def uninstall(self, pkg: WebApplication, root_password: str, watcher: ProcessWatcher) -> bool: + self.logger.info("Checking if {} installation directory {} exists".format(pkg.name, pkg.installation_dir)) + + if not os.path.exists(pkg.installation_dir): + watcher.show_message(title=self.i18n['error'], + body=self.i18n['web.uninstall.error.install_dir.not_found'].format(bold(pkg.installation_dir)), + type_=MessageType.ERROR) + return False + + self.logger.info("Removing {} installation directory {}".format(pkg.name, pkg.installation_dir)) + try: + shutil.rmtree(pkg.installation_dir) + except: + watcher.show_message(title=self.i18n['error'], + body=self.i18n['web.uninstall.error.remove'].format(bold(pkg.installation_dir)), + type_=MessageType.ERROR) + traceback.print_exc() + return False + + self.logger.info("Checking if {} desktop entry file {} exists".format(pkg.name, pkg.desktop_entry)) + if os.path.exists(pkg.desktop_entry): + try: + os.remove(pkg.desktop_entry) + except: + watcher.show_message(title=self.i18n['error'], + body=self.i18n['web.uninstall.error.remove'].format(bold(pkg.desktop_entry)), + type_=MessageType.ERROR) + traceback.print_exc() + return False + + return True def get_managed_types(self) -> Set[Type[SoftwarePackage]]: return {WebApplication} - def get_info(self, pkg: SoftwarePackage) -> dict: - pass + def get_info(self, pkg: WebApplication) -> dict: + if pkg.installed: + info = {'{}_{}'.format(idx + 1, att): getattr(pkg, att) for idx, att in enumerate(('url', 'description', 'version', 'installation_dir', 'desktop_entry'))} + info['6_exec_file'] = pkg.get_exec_path() + info['7_icon_path'] = pkg.get_disk_icon_path() + return info def get_history(self, pkg: SoftwarePackage) -> PackageHistory: pass @@ -97,8 +153,78 @@ class WebApplicationManager(SoftwareManager): watcher.show_message(title=self.i18n['error'], body=self.i18n['web.env.error'].format(bold(pkg.name)), type_=MessageType.ERROR) return False + Path(INSTALLED_PATH).mkdir(parents=True, exist_ok=True) + + pkg_name = pkg.name.lower() + + app_name_id, app_dir = pkg_name, '{}/{}'.format(INSTALLED_PATH, pkg_name) + + counter = 1 + while True: + if not os.path.exists(app_dir): + break + else: + app_name_id += str(counter) + app_dir = '{}/{}'.format(INSTALLED_PATH, app_name_id) + counter += 1 + + installed = handler.handle_simple(nativefier.install(url=pkg.url, name=pkg_name, output_dir=app_dir, + electron_version=self.env_settings['electron']['version'], cwd=INSTALLED_PATH)) + + if not installed: + msg = '{}.{}.'.format(self.i18n['wen.install.error'].format(bold(pkg.name)), + self.i18n['web.install.nativefier.error.unknown'].format(bold(self.i18n['details'].capitalize()))) + watcher.show_message(title=self.i18n['error'], body=msg, type_=MessageType.ERROR) + return False + + inner_dir = os.listdir(app_dir) + + if not inner_dir: + msg = '{}.{}.'.format(self.i18n['wen.install.error'].format(bold(pkg.name)), + self.i18n['web.install.nativefier.error.inner_dir'].format(bold(app_dir))) + watcher.show_message(title=self.i18n['error'], body=msg, type_=MessageType.ERROR) + return False + + # bringing the inner app folder to the 'installed' folder level: + inner_dir = '{}/{}'.format(app_dir, inner_dir[0]) + temp_dir = '{}/tmp_{}'.format(INSTALLED_PATH, app_name_id) + os.rename(inner_dir, temp_dir) + shutil.rmtree(app_dir) + os.rename(temp_dir, app_dir) + + pkg.installation_dir = app_dir + + version_path = '{}/version'.format(app_dir) + + if os.path.exists(version_path): + with open(version_path, 'r') as f: + pkg.version = f.read().strip() + pkg.latest_version = pkg.version + + desktop_entry = DESKTOP_ENTRY_PATH_PATTERN.format(name=app_name_id) + entry_content = self._gen_desktop_entry_content(pkg) + + # TODO check if the desktop entry exists + + with open(desktop_entry, 'w+') as f: + f.write(entry_content) + + pkg.desktop_entry = desktop_entry + return True + def _gen_desktop_entry_content(self, pkg: WebApplication) -> str: + return """ + [Desktop Entry] + Type=Application + Name={name} ( web ) + Categories=Applications; + Comment={desc} + Icon={icon} + Exec={exec_path} + """.format(name=pkg.name, exec_path=pkg.get_exec_path(), + desc=pkg.description or pkg.url, icon=pkg.get_disk_icon_path()) + def is_enabled(self) -> bool: return self.enabled @@ -112,7 +238,13 @@ class WebApplicationManager(SoftwareManager): return False def _update_environment(self, handler: ProcessHandler = None) -> bool: - return self.node_updater.update_environment(self.context.is_system_x86_64(), handler=handler) + updated_settings = self.node_updater.update_environment(self.context.is_system_x86_64(), handler=handler) + + if updated_settings is not None: + self.env_settings = updated_settings + return True + + return False def prepare(self): if bool(int(os.getenv('BAUH_WEB_UPDATE_NODE', 1))): @@ -134,8 +266,8 @@ class WebApplicationManager(SoftwareManager): def is_default_enabled(self) -> bool: return True - def launch(self, pkg: SoftwarePackage): - pass + def launch(self, pkg: WebApplication): + subprocess.Popen(pkg.get_exec_path()) def get_screenshots(self, pkg: SoftwarePackage) -> List[str]: pass diff --git a/bauh/gems/web/environment.py b/bauh/gems/web/environment.py index 73976bf8..3fbc7f6f 100644 --- a/bauh/gems/web/environment.py +++ b/bauh/gems/web/environment.py @@ -163,12 +163,11 @@ class EnvironmentUpdater: if self._download_electron(version=version, is_x86_x64_arch=is_x86_x64_arch, watcher=watcher): return self._download_electron_sha256(version=version, watcher=watcher) else: - files = run_cmd('ls', print_error=False, cwd=ELECTRON_PATH) + files = os.listdir(ELECTRON_PATH) if files: - file_list = files.split('\n') file_name = ELECTRON_DOWNLOAD_URL.format(version=version, arch='x64' if is_x86_x64_arch else 'ia32').split('/')[-1] - electron_file = [f for f in file_list if f == file_name] + electron_file = [f for f in files if f == file_name] if electron_file: self.logger.info("Electron {} already downloaded".format(version)) @@ -177,7 +176,7 @@ class EnvironmentUpdater: return False file_name = ELECTRON_SHA256_URL.split('/')[-1] - sha256_file = [f for f in file_list if f == file_name] + sha256_file = [f for f in files if f == file_name] if sha256_file: self.logger.info("Electron {} sha256 already downloaded".format(version)) @@ -191,37 +190,44 @@ class EnvironmentUpdater: return False - def update_environment(self, is_x86_x64_arch: bool, handler: ProcessHandler = None) -> bool: + def get_environment(self) -> dict: try: res = self.http_client.get(URL_ENVIRONMENT_SETTINGS) if not res: self.logger.warning('Could not retrieve the environments settings from the cloud') - return False + return try: - settings = yaml.safe_load(res.content) + return yaml.safe_load(res.content) except yaml.YAMLError: self.logger.error('Could not parse environment settings: {}'.format(res.text)) - return False - - if not self.update_node(version=settings['nodejs']['version'], version_url=settings['nodejs']['url'], - watcher=handler.watcher if handler else None): - self.logger.warning('Could not install / update NodeJS') - return False - - if not self.install_nativefier(version=settings['nativefier']['version'], handler=handler): - self.logger.warning('Could not install / update nativefier') - return False - - res = self.install_electron(version=settings['electron']['version'], is_x86_x64_arch=is_x86_x64_arch, - watcher=handler.watcher if handler else None) - - if res: - self.logger.info('Environment updated') - else: - self.logger.warning('Could not update the environment') - - return res + return except requests.exceptions.ConnectionError: - return False + return + + def update_environment(self, is_x86_x64_arch: bool, handler: ProcessHandler = None) -> dict: + + settings = self.get_environment() + + if settings is None: + return + + if not self.update_node(version=settings['nodejs']['version'], version_url=settings['nodejs']['url'], + watcher=handler.watcher if handler else None): + self.logger.warning('Could not install / update NodeJS') + return + + if not self.install_nativefier(version=settings['nativefier']['version'], handler=handler): + self.logger.warning('Could not install / update nativefier') + return + + res = self.install_electron(version=settings['electron']['version'], is_x86_x64_arch=is_x86_x64_arch, + watcher=handler.watcher if handler else None) + + if res: + self.logger.info('Environment updated') + else: + self.logger.warning('Could not update the environment') + + return settings diff --git a/bauh/gems/web/model.py b/bauh/gems/web/model.py index 43f0c7b4..5539be6c 100644 --- a/bauh/gems/web/model.py +++ b/bauh/gems/web/model.py @@ -6,19 +6,31 @@ from bauh.gems.web import ROOT_DIR class WebApplication(SoftwarePackage): - def __init__(self, url: str, name: str, description: str, icon_url: str): - super(WebApplication, self).__init__(id=url, name=name, description=description, icon_url=icon_url) + def __init__(self, url: str = None, name: str = None, description: str = None, icon_url: str = None, + installation_dir: str = None, desktop_entry: str = None, installed: bool = False, version: str = None): + super(WebApplication, self).__init__(id=url, name=name, description=description, + icon_url=icon_url, installed=installed, version=version) self.url = url + self.installation_dir = installation_dir + self.desktop_entry = desktop_entry def has_history(self): return False def has_info(self): - return False + return self.installed + + @staticmethod + def _get_cached_attrs() -> tuple: + return 'name', 'version', 'url', 'description', 'icon_url', 'installation_dir', 'desktop_entry' def can_be_downgraded(self): return False + def get_exec_path(self) -> str: + if self.installation_dir: + return '{}/{}'.format(self.installation_dir, self.installation_dir.split('/')[-1]) + def get_type(self): return 'web' @@ -32,22 +44,32 @@ class WebApplication(SoftwarePackage): return True def supports_disk_cache(self): - return self.installed and self.is_application() + return self.installed def get_disk_cache_path(self): - return CACHE_PATH + '/' + self.get_type() + return self.installation_dir def get_data_to_cache(self) -> dict: - """ - :return: the application data that should be cached in disk / memory for quick access - """ - pass + data = {} + + for attr in self._get_cached_attrs(): + if hasattr(self, attr): + val = getattr(self, attr) + + if val is not None: + data[attr] = val + + return data def fill_cached_data(self, data: dict): - pass + for attr in self._get_cached_attrs(): + val = data.get(attr) + + if val and hasattr(self, attr): + setattr(self, attr, val) def can_be_run(self) -> bool: - return self.installed + return self.installed and self.installation_dir def is_trustable(self) -> bool: return False diff --git a/bauh/gems/web/nativefier.py b/bauh/gems/web/nativefier.py new file mode 100644 index 00000000..1c4f71b3 --- /dev/null +++ b/bauh/gems/web/nativefier.py @@ -0,0 +1,6 @@ +from bauh.commons.system import SimpleProcess +from bauh.gems.web import NATIVEFIER_BIN_PATH + + +def install(url: str, name: str, output_dir: str, electron_version: str, cwd: str) -> SimpleProcess: + return SimpleProcess([NATIVEFIER_BIN_PATH, url, '--name', name, '-e', electron_version, output_dir], cwd=cwd) diff --git a/bauh/gems/web/resources/locale/en b/bauh/gems/web/resources/locale/en index c0c7bba7..b31634d5 100644 --- a/bauh/gems/web/resources/locale/en +++ b/bauh/gems/web/resources/locale/en @@ -1,5 +1,17 @@ gem.web.info=It allows to install Web applications on your system web.environment.install=Installing {} web.waiting.env_updater=Updating environment -web.env.checking=Checking the Web application installation environment -web.env.error=It seems there are issues with the Web application installation environment. It wil not be possible to install {}. \ No newline at end of file +web.env.checking=Checking the Web installation environment +web.env.error=It seems there are issues with the Web installation environment. It wil not be possible to install {}. +web.install.error=An error has happened during the {} installation +web.install.nativefier.error.unknown=Have a look at the {} to identify the reason +web.install.nativefier.error.inner_dir=The app installation directory was not found in {} +web.uninstall.error.install_dir.not_found=The installation directory {} was not found +web.uninstall.error.remove_dir=It was not possible to remove {} +web.info.1_url=URL +web.info.2_description=description +web.info.3_version=version +web.info.4_installation_dir=installation dir +web.info.5_desktop_entry=shortcut +web.info.6_exec_file=executable +web.info.7_icon_path=icon \ No newline at end of file diff --git a/bauh/gems/web/resources/locale/pt b/bauh/gems/web/resources/locale/pt index f1999241..ae8b1df8 100644 --- a/bauh/gems/web/resources/locale/pt +++ b/bauh/gems/web/resources/locale/pt @@ -1,5 +1,17 @@ gem.web.info=Permite instalar aplicações Web no seu sistema web.environment.nativefier=Instalando {} web.waiting.env_updater=Atualizando ambiente -web.env.checking=Verificando o ambiente de instalação para aplicativos Web -web.env.error=Parece haver problemas com o ambiente de instalação para aplicativos Web. Não sera possível instalar {}. \ No newline at end of file +web.env.checking=Verificando o ambiente de instalação Web +web.env.error=Parce que existem problemas com o ambiente de instalação Web. Não será possível instalar {}. +wen.install.error=Ocorreu um erro durante a instalação de {} +web.install.nativefier.error.unknown=Dê uma olhada em {} para identificar o motivo +web.install.nativefier.error.inner_dir=O diretório de instalação do aplicativo não foi encontrado em {} +web.uninstall.error.install_dir.not_found=O diretório de instalação {} não foi encontrado +web.uninstall.error.remove=Não foi possível remover {} +web.info.1_url=URL +web.info.2_description=descrição +web.info.3_version=versão +web.info.4_installation_dir=diretório de instalação +web.info.5_desktop_entry=atalho +web.info.6_exec_file=executável +web.info.7_icon_path=ícone \ No newline at end of file diff --git a/bauh/view/resources/locale/ca b/bauh/view/resources/locale/ca index 6f557128..efdbb27b 100644 --- a/bauh/view/resources/locale/ca +++ b/bauh/view/resources/locale/ca @@ -201,4 +201,5 @@ publisher.verified=verificat mirror=mirall emulator=emulador do_not.install=no instal·leu -repository=dipòsit \ No newline at end of file +repository=dipòsit +details=detalls \ No newline at end of file diff --git a/bauh/view/resources/locale/de b/bauh/view/resources/locale/de index 06f1a45f..83320ae1 100644 --- a/bauh/view/resources/locale/de +++ b/bauh/view/resources/locale/de @@ -155,4 +155,5 @@ close=Schließen publisher.verified=Verifiziert emulator=emulator do_not.install=nicht installieren -repository=repository \ No newline at end of file +repository=repository +details=details \ No newline at end of file diff --git a/bauh/view/resources/locale/en b/bauh/view/resources/locale/en index 72a26c48..16139eb2 100644 --- a/bauh/view/resources/locale/en +++ b/bauh/view/resources/locale/en @@ -157,3 +157,4 @@ mirror=mirror emulator=emulator do_not.install=don't install repository=repository +details=details \ No newline at end of file diff --git a/bauh/view/resources/locale/es b/bauh/view/resources/locale/es index c3324dc0..c0741bc2 100644 --- a/bauh/view/resources/locale/es +++ b/bauh/view/resources/locale/es @@ -200,4 +200,5 @@ publisher.verified=verificado mirror=espejo emulator=emulador do_not.install=no instalar -repository=repositorio \ No newline at end of file +repository=repositorio +details=detalles \ No newline at end of file diff --git a/bauh/view/resources/locale/it b/bauh/view/resources/locale/it index 8a3043e6..afd20863 100644 --- a/bauh/view/resources/locale/it +++ b/bauh/view/resources/locale/it @@ -156,4 +156,5 @@ publisher.verified=verificato mirror=specchio emulator=emulatore do_not.install=non installare -repository=deposito \ No newline at end of file +repository=deposito +details=dettagli \ No newline at end of file diff --git a/bauh/view/resources/locale/pt b/bauh/view/resources/locale/pt index 7a3afc7f..c9b89f08 100644 --- a/bauh/view/resources/locale/pt +++ b/bauh/view/resources/locale/pt @@ -203,4 +203,5 @@ publisher.verified=verificado mirror=espelho emulator=emulador do_not.install=não instalar -repository=repositório \ No newline at end of file +repository=repositório +details=detalhes \ No newline at end of file diff --git a/setup.py b/setup.py index d9ac265b..24c0fa53 100644 --- a/setup.py +++ b/setup.py @@ -3,11 +3,11 @@ import os from setuptools import setup, find_packages DESCRIPTION = ( - "Graphical interface to manage Flatpak, Snap, AppImage and AUR packages" + "Graphical interface to manage Flatpak, Snap, AppImage, AUR and Web applications" ) -AUTHOR = "Vinicius Moreira" -AUTHOR_EMAIL = "vinicius_fmoreira@hotmail.com" +AUTHOR = "bauh developers" +AUTHOR_EMAIL = "bauh4linux@gmail.com" NAME = 'bauh' URL = "https://github.com/vinifmor/" + NAME