[snap] improvement -> full support refactored to use the Snapd socket instead of the Ubuntu's old Snap API

This commit is contained in:
Vinicius Moreira
2020-08-26 12:54:56 -03:00
parent 0194443470
commit 50893bff99
16 changed files with 347 additions and 419 deletions

View File

@@ -48,6 +48,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Flatpak - Flatpak
- creating the exports path **~/.local/share/flatpak/exports/share** (if it does not exist) and adding it to install/upgrade/downgrade/remove commands path to prevent warning messages. [#128](https://github.com/vinifmor/bauh/issues/128) - creating the exports path **~/.local/share/flatpak/exports/share** (if it does not exist) and adding it to install/upgrade/downgrade/remove commands path to prevent warning messages. [#128](https://github.com/vinifmor/bauh/issues/128)
- downgrade function refactored - downgrade function refactored
- Snap
- full support refactored to use the Snapd socket instead of the Ubuntu's old Snap API (which was recently disabled). Now the 'read' operations are faster, a only the icon is cached to the disk.
- minor UI improvements - minor UI improvements
@@ -80,6 +82,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- some environment variables are not available during the common operations (install, upgrade, downgrade, uninstall, launch) - some environment variables are not available during the common operations (install, upgrade, downgrade, uninstall, launch)
- minor fixes - minor fixes
- Snap - Snap
- not able to install classic Snaps due to Ubuntu's old Snaps API shutdown
- some environment variables are not available during the common operations (install, upgrade, downgrade, uninstall, launch) - some environment variables are not available during the common operations (install, upgrade, downgrade, uninstall, launch)
- Web - Web
- some environment variable are not available during the launch process - some environment variable are not available during the launch process

View File

@@ -3,7 +3,7 @@ import os
import shutil import shutil
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from pathlib import Path from pathlib import Path
from typing import List, Set, Type, Tuple from typing import List, Set, Type, Tuple, Optional
import yaml import yaml
@@ -17,7 +17,7 @@ from bauh.api.abstract.view import ViewComponent
class SearchResult: class SearchResult:
def __init__(self, installed: List[SoftwarePackage], new: List[SoftwarePackage], total: int): def __init__(self, installed: Optional[List[SoftwarePackage]], new: Optional[List[SoftwarePackage]], total: int):
""" """
:param installed: already installed packages :param installed: already installed packages
:param new: new packages found :param new: new packages found

View File

@@ -1 +0,0 @@
SNAP_API_URL = 'https://search.apps.ubuntu.com/api/v1'

View File

@@ -1,25 +1,23 @@
import re import re
import time import time
from datetime import datetime
from threading import Thread from threading import Thread
from typing import List, Set, Type from typing import List, Set, Type, Optional
from bauh.api.abstract.controller import SoftwareManager, SearchResult, ApplicationContext, UpgradeRequirements, \ from bauh.api.abstract.controller import SoftwareManager, SearchResult, ApplicationContext, UpgradeRequirements, \
TransactionResult TransactionResult
from bauh.api.abstract.disk import DiskCacheLoader from bauh.api.abstract.disk import DiskCacheLoader
from bauh.api.abstract.handler import ProcessWatcher, TaskManager from bauh.api.abstract.handler import ProcessWatcher, TaskManager
from bauh.api.abstract.model import SoftwarePackage, PackageHistory, PackageUpdate, PackageSuggestion, \ from bauh.api.abstract.model import SoftwarePackage, PackageHistory, PackageUpdate, PackageSuggestion, \
SuggestionPriority, CustomSoftwareAction SuggestionPriority, CustomSoftwareAction, PackageStatus
from bauh.api.abstract.view import SingleSelectComponent, SelectViewType, InputOption from bauh.api.abstract.view import SingleSelectComponent, SelectViewType, InputOption
from bauh.commons import resource, internet from bauh.commons import resource, internet
from bauh.commons.category import CategoriesDownloader from bauh.commons.category import CategoriesDownloader
from bauh.commons.html import bold from bauh.commons.html import bold
from bauh.commons.system import SystemProcess, ProcessHandler, new_root_subprocess from bauh.commons.system import SystemProcess, ProcessHandler, new_root_subprocess, get_human_size_str
from bauh.gems.snap import snap, URL_CATEGORIES_FILE, SNAP_CACHE_PATH, CATEGORIES_FILE_PATH, SUGGESTIONS_FILE, \ from bauh.gems.snap import snap, URL_CATEGORIES_FILE, SNAP_CACHE_PATH, CATEGORIES_FILE_PATH, SUGGESTIONS_FILE, \
get_icon_path get_icon_path, snapd
from bauh.gems.snap.constants import SNAP_API_URL
from bauh.gems.snap.model import SnapApplication from bauh.gems.snap.model import SnapApplication
from bauh.gems.snap.worker import SnapAsyncDataLoader from bauh.gems.snap.snapd import SnapdClient
RE_AVAILABLE_CHANNELS = re.compile(re.compile(r'(\w+)\s+(snap install.+)')) RE_AVAILABLE_CHANNELS = re.compile(re.compile(r'(\w+)\s+(snap install.+)'))
@@ -47,36 +45,12 @@ class SnapManager(SoftwareManager):
i18n_confirm_key='snap.action.refresh.confirm') i18n_confirm_key='snap.action.refresh.confirm')
] ]
def get_info_path(self) -> str: def _fill_categories(self, app: SnapApplication):
if self.info_path is None:
self.info_path = snap.get_app_info_path()
return self.info_path
def map_json(self, app_json: dict, installed: bool, disk_loader: DiskCacheLoader, internet: bool = True) -> SnapApplication:
app = SnapApplication(publisher=app_json.get('publisher'),
rev=app_json.get('rev'),
notes=app_json.get('notes'),
has_apps_field=app_json.get('apps_field', False),
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', app_json.get('summary')),
verified_publisher=app_json.get('developer_validation', '') == 'verified',
extra_actions=self.custom_actions)
if app.publisher and app.publisher.endswith('*'):
app.verified_publisher = True
app.publisher = app.publisher.replace('*', '')
categories = self.categories.get(app.name.lower()) categories = self.categories.get(app.name.lower())
if categories: if categories:
app.categories = categories app.categories = categories
app.installed = installed
if not app.is_application(): if not app.is_application():
categories = app.categories categories = app.categories
@@ -87,70 +61,67 @@ class SnapManager(SoftwareManager):
if 'runtime' not in categories: if 'runtime' not in categories:
categories.append('runtime') categories.append('runtime')
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)
if internet:
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, is_url: bool = False) -> SearchResult: def search(self, words: str, disk_loader: DiskCacheLoader, limit: int = -1, is_url: bool = False) -> SearchResult:
if is_url: if is_url or (not snap.is_installed() and not snapd.is_running()):
return SearchResult([], [], 0) return SearchResult([], [], 0)
if snap.is_snapd_running(): snapd_client = SnapdClient(self.logger)
apps_found = snapd_client.query(words)
res = SearchResult([], [], 0)
if apps_found:
installed = self.read_installed(disk_loader).installed installed = self.read_installed(disk_loader).installed
res = SearchResult([], [], 0) for app_json in apps_found:
for app_json in snap.search(words):
already_installed = None already_installed = None
if installed: if installed:
already_installed = [i for i in installed if i.id == app_json.get('name')] already_installed = [i for i in installed if i.id == app_json.get('id')]
already_installed = already_installed[0] if already_installed else None already_installed = already_installed[0] if already_installed else None
if already_installed: if already_installed:
res.installed.append(already_installed) res.installed.append(already_installed)
else: else:
res.new.append(self.map_json(app_json, installed=False, disk_loader=disk_loader)) res.new.append(self._map_to_app(app_json, installed=False))
res.total = len(res.installed) + len(res.new) res.total = len(res.installed) + len(res.new)
return res return res
else:
return SearchResult([], [], 0)
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: 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:
info_path = self.get_info_path() if snap.is_installed() and snapd.is_running():
snapd_client = SnapdClient(self.logger)
if snap.is_snapd_running() and info_path: app_names = {a['snap'] for a in snapd_client.list_only_apps()}
installed = [self.map_json(app_json, installed=True, disk_loader=disk_loader, internet=internet_available) for app_json in snap.read_installed(info_path)] installed = [self._map_to_app(app_json=appjson,
installed=True,
disk_loader=disk_loader,
is_application=app_names and appjson['name'] in app_names) for appjson in snapd_client.list_all_snaps()]
return SearchResult(installed, None, len(installed)) return SearchResult(installed, None, len(installed))
else: else:
return SearchResult([], None, 0) return SearchResult([], None, 0)
def downgrade(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher) -> bool: def downgrade(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher) -> bool:
if not snap.is_installed():
watcher.print("'snap' seems not to be installed")
return False
if not snapd.is_running():
watcher.print("'snapd' seems not to be running")
return False
return ProcessHandler(watcher).handle_simple(snap.downgrade_and_stream(pkg.name, root_password))[0] return ProcessHandler(watcher).handle_simple(snap.downgrade_and_stream(pkg.name, root_password))[0]
def upgrade(self, requirements: UpgradeRequirements, root_password: str, watcher: ProcessWatcher) -> SystemProcess: def upgrade(self, requirements: UpgradeRequirements, root_password: str, watcher: ProcessWatcher) -> SystemProcess:
raise Exception("'upgrade' is not supported by {}".format(SnapManager.__class__.__name__)) raise Exception("'upgrade' is not supported by {}".format(SnapManager.__class__.__name__))
def uninstall(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher, disk_loader: DiskCacheLoader) -> TransactionResult: def uninstall(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher, disk_loader: DiskCacheLoader) -> TransactionResult:
uninstalled = ProcessHandler(watcher).handle_simple(snap.uninstall_and_stream(pkg.name, root_password))[0] if snap.is_installed() and snapd.is_running():
uninstalled = ProcessHandler(watcher).handle_simple(snap.uninstall_and_stream(pkg.name, root_password))[0]
if uninstalled: if uninstalled:
if self.suggestions_cache: if self.suggestions_cache:
self.suggestions_cache.delete(pkg.name) self.suggestions_cache.delete(pkg.name)
return TransactionResult(success=True, installed=None, removed=[pkg]) return TransactionResult(success=True, installed=None, removed=[pkg])
return TransactionResult.fail() return TransactionResult.fail()
@@ -162,17 +133,29 @@ class SnapManager(SoftwareManager):
self.api_cache.delete(pkg.id) self.api_cache.delete(pkg.id)
def get_info(self, pkg: SnapApplication) -> dict: def get_info(self, pkg: SnapApplication) -> dict:
info = snap.get_info(pkg.name, attrs=('license', 'contact', 'commands', 'snap-id', 'tracking', 'installed')) info = {
info['description'] = pkg.description 'description': pkg.description,
info['publisher'] = pkg.publisher 'developer': pkg.developer,
info['revision'] = pkg.rev 'license': pkg.license,
info['name'] = pkg.name 'contact': pkg.contact,
'snap-id': pkg.id,
'name': pkg.name,
'publisher': pkg.publisher,
'revision': pkg.rev,
'tracking': pkg.tracking,
'channel': pkg.channel,
'type': pkg.type
}
if info.get('commands'): if pkg.installed:
info['commands'] = ' '.join(info['commands']) commands = [*{c['name'] for c in SnapdClient(self.logger).list_commands(pkg.name)}]
commands.sort()
info['commands'] = commands
if info.get('license') and info['license'] == 'unset': if pkg.installed_size:
del info['license'] info['installed_size']: get_human_size_str(pkg.installed_size)
elif pkg.download_size:
info['download_size'] = get_human_size_str(pkg.download_size)
return info return info
@@ -180,13 +163,16 @@ class SnapManager(SoftwareManager):
raise Exception("'get_history' is not supported by {}".format(pkg.__class__.__name__)) raise Exception("'get_history' is not supported by {}".format(pkg.__class__.__name__))
def install(self, pkg: SnapApplication, root_password: str, disk_loader: DiskCacheLoader, watcher: ProcessWatcher) -> TransactionResult: def install(self, pkg: SnapApplication, root_password: str, disk_loader: DiskCacheLoader, watcher: ProcessWatcher) -> TransactionResult:
info_path = self.get_info_path()
if not info_path:
self.logger.warning('Information directory was not found. It will not be possible to determine if the installed application can be launched')
# retrieving all installed so it will be possible to know the additional installed runtimes after the operation succeeds # retrieving all installed so it will be possible to know the additional installed runtimes after the operation succeeds
installed_names = snap.list_installed_names() if not snap.is_installed():
watcher.print("'snap' seems not to be installed")
return TransactionResult.fail()
if not snapd.is_running():
watcher.print("'snapd' seems not to be running")
return TransactionResult.fail()
installed_names = {s['name'] for s in SnapdClient(self.logger).list_all_snaps()}
res, output = ProcessHandler(watcher).handle_simple(snap.install_and_stream(pkg.name, pkg.confinement, root_password)) res, output = ProcessHandler(watcher).handle_simple(snap.install_and_stream(pkg.name, pkg.confinement, root_password))
@@ -208,17 +194,10 @@ class SnapManager(SoftwareManager):
deny_label=self.i18n['cancel']): deny_label=self.i18n['cancel']):
self.logger.info("Installing '{}' with the custom command '{}'".format(pkg.name, channel_select.value)) self.logger.info("Installing '{}' with the custom command '{}'".format(pkg.name, channel_select.value))
res = ProcessHandler(watcher).handle(SystemProcess(new_root_subprocess(channel_select.value.value.split(' '), root_password=root_password))) res = ProcessHandler(watcher).handle(SystemProcess(new_root_subprocess(channel_select.value.value.split(' '), root_password=root_password)))
if res and info_path:
pkg.has_apps_field = snap.has_apps_field(pkg.name, info_path)
return self._gen_installation_response(success=res, pkg=pkg, return self._gen_installation_response(success=res, pkg=pkg,
installed=installed_names, disk_loader=disk_loader) installed=installed_names, disk_loader=disk_loader)
else: else:
self.logger.error("Could not find available channels in the installation output: {}".format(output)) self.logger.error("Could not find available channels in the installation output: {}".format(output))
else:
if info_path:
pkg.has_apps_field = snap.has_apps_field(pkg.name, info_path)
return self._gen_installation_response(success=res, pkg=pkg, installed=installed_names, disk_loader=disk_loader) return self._gen_installation_response(success=res, pkg=pkg, installed=installed_names, disk_loader=disk_loader)
@@ -278,7 +257,7 @@ class SnapManager(SoftwareManager):
def list_warnings(self, internet_available: bool) -> List[str]: def list_warnings(self, internet_available: bool) -> List[str]:
if snap.is_installed(): if snap.is_installed():
if not snap.is_snapd_running(): if not snapd.is_running():
snap_bold = bold('Snap') snap_bold = bold('Snap')
return [self.i18n['snap.notification.snapd_unavailable'].format(bold('snapd'), snap_bold), return [self.i18n['snap.notification.snapd_unavailable'].format(bold('snapd'), snap_bold),
self.i18n['snap.notification.snap.disable'].format(snap_bold, bold('{} > {}'.format(self.i18n['settings'].capitalize(), self.i18n['snap.notification.snap.disable'].format(snap_bold, bold('{} > {}'.format(self.i18n['settings'].capitalize(),
@@ -291,24 +270,57 @@ class SnapManager(SoftwareManager):
self.logger.warning('It seems Snap API is not available. Search output: {}'.format(output)) self.logger.warning('It seems Snap API is not available. Search output: {}'.format(output))
return [self.i18n['snap.notifications.api.unavailable'].format(bold('Snaps'), bold('Snap'))] return [self.i18n['snap.notifications.api.unavailable'].format(bold('Snaps'), bold('Snap'))]
def _fill_suggestion(self, pkg_name: str, priority: SuggestionPriority, out: List[PackageSuggestion]): def _fill_suggestion(self, name: str, priority: SuggestionPriority, snapd_client: SnapdClient, out: List[PackageSuggestion]):
res = self.http_client.get_json(SNAP_API_URL + '/search?q=package_name:{}'.format(pkg_name)) res = snapd_client.find_by_name(name)
if res and res['_embedded']['clickindex:package']: if res:
pkg = res['_embedded']['clickindex:package'][0] if len(res) == 1:
pkg['rev'] = pkg['revision'] app_json = res[0]
pkg['name'] = pkg_name else:
jsons_found = [p for p in res if p['name'] == name]
app_json = jsons_found[0] if jsons_found else None
sug = PackageSuggestion(self.map_json(pkg, installed=False, disk_loader=None), priority) if app_json:
self.suggestions_cache.add(pkg_name, sug) sug = PackageSuggestion(self._map_to_app(app_json, False), priority)
out.append(sug) self.suggestions_cache.add(name, sug)
else: out.append(sug)
self.logger.warning("Could not retrieve suggestion '{}'".format(pkg_name)) return
self.logger.warning("Could not retrieve suggestion '{}'".format(name))
def _map_to_app(self, app_json: dict, installed: bool, disk_loader: Optional[DiskCacheLoader] = None, is_application: bool = False) -> SnapApplication:
app = SnapApplication(id=app_json.get('id'),
name=app_json.get('name'),
license=app_json.get('license'),
version=app_json.get('version'),
latest_version=app_json.get('version'),
description=app_json.get('description', app_json.get('summary')),
installed=installed,
rev=app_json.get('revision'),
publisher=app_json['publisher'].get('display-name', app_json['publisher'].get('username')),
verified_publisher=app_json['publisher'].get('validation') == 'verified',
icon_url=app_json.get('icon'),
screenshots={m['url'] for m in app_json.get('media', []) if m['type'] == 'screenshot'},
download_size=app_json.get('download-size'),
channel=app_json.get('channel'),
confinement=app_json.get('confinement'),
app_type=app_json.get('type'),
app=is_application,
installed_size=app_json.get('installed-size'),
extra_actions=self.custom_actions)
if disk_loader and app.installed:
disk_loader.fill(app)
self._fill_categories(app)
app.status = PackageStatus.READY
return app
def list_suggestions(self, limit: int, filter_installed: bool) -> List[PackageSuggestion]: def list_suggestions(self, limit: int, filter_installed: bool) -> List[PackageSuggestion]:
res = [] res = []
if snap.is_snapd_running(): if snapd.is_running():
self.logger.info('Downloading suggestions file {}'.format(SUGGESTIONS_FILE)) self.logger.info('Downloading suggestions file {}'.format(SUGGESTIONS_FILE))
file = self.http_client.get(SUGGESTIONS_FILE) file = self.http_client.get(SUGGESTIONS_FILE)
@@ -319,7 +331,8 @@ class SnapManager(SoftwareManager):
self.logger.info('Mapping suggestions') self.logger.info('Mapping suggestions')
suggestions, threads = [], [] suggestions, threads = [], []
installed = {i.name.lower() for i in self.read_installed(disk_loader=None).installed} if filter_installed else None snapd_client = SnapdClient(self.logger)
installed = {s['name'].lower() for s in snapd_client.list_all_snaps()}
for l in file.text.split('\n'): for l in file.text.split('\n'):
if l: if l:
@@ -333,7 +346,7 @@ class SnapManager(SoftwareManager):
if cached_sug: if cached_sug:
res.append(cached_sug) res.append(cached_sug)
else: else:
t = Thread(target=self._fill_suggestion, args=(name, SuggestionPriority(int(sug[0])), res)) t = Thread(target=self._fill_suggestion, args=(name, SuggestionPriority(int(sug[0])), snapd_client, res))
t.start() t.start()
threads.append(t) threads.append(t)
time.sleep(0.001) # to avoid being blocked time.sleep(0.001) # to avoid being blocked
@@ -350,22 +363,21 @@ class SnapManager(SoftwareManager):
return True return True
def launch(self, pkg: SnapApplication): def launch(self, pkg: SnapApplication):
snap.run(pkg, self.context.logger) commands = SnapdClient(self.logger).list_commands(pkg.name)
def get_screenshots(self, pkg: SoftwarePackage) -> List[str]: if commands:
res = self.http_client.get_json('{}/search?q={}'.format(SNAP_API_URL, pkg.name)) if len(commands) == 1:
cmd = commands[0]['name']
if res:
if res.get('_embedded') and res['_embedded'].get('clickindex:package'):
snap_data = res['_embedded']['clickindex:package'][0]
if snap_data.get('screenshot_urls'):
return snap_data['screenshot_urls']
else:
self.logger.warning("No 'screenshots_urls' defined for {}".format(pkg))
else: else:
self.logger.error('It seems the API is returning a different response: {}'.format(res)) desktop_cmd = [c for c in commands if 'desktop-file' in c]
else:
self.logger.warning('Could not retrieve data for {}'.format(pkg))
return [] if desktop_cmd:
cmd = desktop_cmd[0]['name']
else:
cmd = commands[0]['name']
self.logger.info("Running '{}': {}".format(pkg.name, cmd))
snap.run(cmd)
def get_screenshots(self, pkg: SnapApplication) -> List[str]:
return pkg.screenshots if pkg.has_screenshots() else []

View File

@@ -1,26 +1,46 @@
from typing import List from typing import List, Optional, Set
from bauh.api.abstract.model import SoftwarePackage, CustomSoftwareAction from bauh.api.abstract.model import SoftwarePackage, CustomSoftwareAction
from bauh.commons import resource from bauh.commons import resource
from bauh.gems.snap import ROOT_DIR from bauh.gems.snap import ROOT_DIR
KNOWN_RUNTIME_NAMES = {'snapd', 'snapcraft', 'multipass'}
class SnapApplication(SoftwarePackage): class SnapApplication(SoftwarePackage):
def __init__(self, id: str = None, name: str = None, version: str = None, latest_version: str = None, 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, description: str = None, publisher: str = None, rev: str = None, notes: str = None,
confinement: str = None, has_apps_field: bool = None, verified_publisher: bool = False, extra_actions: List[CustomSoftwareAction] = None): confinement: str = None, verified_publisher: bool = False,
extra_actions: List[CustomSoftwareAction] = None,
screenshots: Optional[Set[str]] = None,
license: Optional[str] = None,
installed: bool = False,
icon_url: Optional[str] = None,
download_size: Optional[int] = None,
developer: Optional[str] = None,
contact: Optional[str] = None,
tracking: Optional[str] = None,
app_type: Optional[str] = None,
channel: Optional[str] = None,
app: bool = False,
installed_size: Optional[int] = None):
super(SnapApplication, self).__init__(id=id, name=name, version=version, super(SnapApplication, self).__init__(id=id, name=name, version=version,
latest_version=latest_version, description=description) latest_version=latest_version, description=description,
license=license, installed=installed, icon_url=icon_url)
self.publisher = publisher self.publisher = publisher
self.rev = rev self.rev = rev
self.notes = notes self.notes = notes
self.confinement = confinement self.confinement = confinement
self.has_apps_field = has_apps_field
self.verified_publisher = verified_publisher self.verified_publisher = verified_publisher
self.extra_actions = extra_actions self.extra_actions = extra_actions
self.screenshots = screenshots
self.download_size = download_size
self.developer = developer
self.contact = contact
self.tracking = tracking
self.type = app_type
self.channel = channel
self.app = app
self.installed_size = installed_size
def supports_disk_cache(self): def supports_disk_cache(self):
return self.installed return self.installed
@@ -44,7 +64,10 @@ class SnapApplication(SoftwarePackage):
return self.get_default_icon_path() return self.get_default_icon_path()
def is_application(self) -> bool: def is_application(self) -> bool:
return not self.installed or ((self.has_apps_field is None or self.has_apps_field) and self.name.lower() not in KNOWN_RUNTIME_NAMES) if self.installed:
return self.app
else:
return self.type == 'app'
def get_disk_cache_path(self): def get_disk_cache_path(self):
return super(SnapApplication, self).get_disk_cache_path() + '/installed/' + self.name return super(SnapApplication, self).get_disk_cache_path() + '/installed/' + self.name
@@ -54,20 +77,17 @@ class SnapApplication(SoftwarePackage):
def get_data_to_cache(self): def get_data_to_cache(self):
return { return {
"icon_url": self.icon_url,
'confinement': self.confinement,
'description': self.description,
'categories': self.categories 'categories': self.categories
} }
def fill_cached_data(self, data: dict): def fill_cached_data(self, data: dict):
if data: if data:
for base_attr in ('icon_url', 'description', 'confinement', 'categories'): for base_attr in self.get_data_to_cache().keys():
if data.get(base_attr): if data.get(base_attr):
setattr(self, base_attr, data[base_attr]) setattr(self, base_attr, data[base_attr])
def can_be_run(self) -> bool: def can_be_run(self) -> bool:
return self.installed and self.is_application() return bool(self.installed and self.is_application())
def get_publisher(self): def get_publisher(self):
return self.publisher return self.publisher
@@ -79,6 +99,9 @@ class SnapApplication(SoftwarePackage):
def supports_backup(self) -> bool: def supports_backup(self) -> bool:
return True return True
def has_screenshots(self) -> bool:
return not self.installed and self.screenshots
def __eq__(self, other): def __eq__(self, other):
if isinstance(other, SnapApplication): if isinstance(other, SnapApplication):
return self.name == other.name return self.name == other.name

View File

@@ -2,15 +2,18 @@ gem.snap.info=Aplicacions publicades a https://snapcraft.io/store
snap.action.refresh.confirm=Actualitza {} ? snap.action.refresh.confirm=Actualitza {} ?
snap.action.refresh.label=Actualitza snap.action.refresh.label=Actualitza
snap.action.refresh.status=Sestà actualitzant snap.action.refresh.status=Sestà actualitzant
snap.info.channel=channel
snap.info.commands=ordres snap.info.commands=ordres
snap.info.contact=contacte snap.info.contact=contacte
snap.info.description=descripció snap.info.description=descripció
snap.info.download_size=Size (Download)
snap.info.installed=instal·lada snap.info.installed=instal·lada
snap.info.installed_size=Size (Instalattion)
snap.info.license=llicència snap.info.license=llicència
snap.info.publisher=publicador snap.info.publisher=publicador
snap.info.revision=revisió snap.info.revision=revisió
snap.info.size=mida
snap.info.tracking=seguiment snap.info.tracking=seguiment
snap.info.type=type
snap.info.version=versió snap.info.version=versió
snap.install.available_channels.help=Seleccioneu-ne un si voleu continuar snap.install.available_channels.help=Seleccioneu-ne un si voleu continuar
snap.install.available_channels.message=No hi ha un canal {} (stable) disponible per a {}. Però més avall nhi ha els següents snap.install.available_channels.message=No hi ha un canal {} (stable) disponible per a {}. Però més avall nhi ha els següents

View File

@@ -2,15 +2,18 @@ gem.snap.info=Anwendungen von https://snapcraft.io/store
snap.action.refresh.confirm=Erneuern {} ? snap.action.refresh.confirm=Erneuern {} ?
snap.action.refresh.label=Erneuern snap.action.refresh.label=Erneuern
snap.action.refresh.status=Erneuern snap.action.refresh.status=Erneuern
snap.info.channel=channel
snap.info.commands=Kommandos snap.info.commands=Kommandos
snap.info.contact=Kontakt snap.info.contact=Kontakt
snap.info.description=Beschreibung snap.info.description=Beschreibung
snap.info.download_size=Size (Download)
snap.info.installed=installiert snap.info.installed=installiert
snap.info.installed_size=Size (Instalattion)
snap.info.license=Lizenz snap.info.license=Lizenz
snap.info.publisher=Herausgeber snap.info.publisher=Herausgeber
snap.info.revision=Revision snap.info.revision=Revision
snap.info.size=Größe
snap.info.tracking=Tracking snap.info.tracking=Tracking
snap.info.type=type
snap.info.version=Version snap.info.version=Version
snap.install.available_channels.help=Wähle einen aus um fortzufahren snap.install.available_channels.help=Wähle einen aus um fortzufahren
snap.install.available_channels.message=Es ist kein {} Channel verfügbar für {}. Es gibt jedoch folgende snap.install.available_channels.message=Es ist kein {} Channel verfügbar für {}. Es gibt jedoch folgende

View File

@@ -2,15 +2,18 @@ gem.snap.info=Applications published at https://snapcraft.io/store
snap.action.refresh.confirm=Refresh {} ? snap.action.refresh.confirm=Refresh {} ?
snap.action.refresh.label=Refresh snap.action.refresh.label=Refresh
snap.action.refresh.status=Refreshing snap.action.refresh.status=Refreshing
snap.info.channel=channel
snap.info.commands=commands snap.info.commands=commands
snap.info.contact=contact snap.info.contact=contact
snap.info.description=description snap.info.description=description
snap.info.download_size=Size (Download)
snap.info.installed=installed snap.info.installed=installed
snap.info.installed_size=Size (Instalattion)
snap.info.license=license snap.info.license=license
snap.info.publisher=publisher snap.info.publisher=publisher
snap.info.revision=revision snap.info.revision=revision
snap.info.size=size
snap.info.tracking=tracking snap.info.tracking=tracking
snap.info.type=type
snap.info.version=version snap.info.version=version
snap.install.available_channels.help=Select one if you want to continue snap.install.available_channels.help=Select one if you want to continue
snap.install.available_channels.message=There is no {} channel available for {}. But there are these below snap.install.available_channels.message=There is no {} channel available for {}. But there are these below

View File

@@ -2,15 +2,18 @@ gem.snap.info=Aplicativos publicados en https://snapcraft.io/store
snap.action.refresh.confirm=Actualizar {} ? snap.action.refresh.confirm=Actualizar {} ?
snap.action.refresh.label=Actualizar snap.action.refresh.label=Actualizar
snap.action.refresh.status=Actualizando snap.action.refresh.status=Actualizando
snap.info.channel=canal
snap.info.commands=comandos snap.info.commands=comandos
snap.info.contact=contacto snap.info.contact=contacto
snap.info.description=descripción snap.info.description=descripción
snap.info.download_size=Tamaño (Descarga)
snap.info.installed=instalado snap.info.installed=instalado
snap.info.installed_size=Tamaño (Instalación)
snap.info.license=licencia snap.info.license=licencia
snap.info.publisher=publicador snap.info.publisher=publicador
snap.info.revision=revisión snap.info.revision=revisión
snap.info.size=tamaño
snap.info.tracking=tracking snap.info.tracking=tracking
snap.info.type=tipo
snap.info.version=versión snap.info.version=versión
snap.install.available_channels.help=Seleccione uno si desea continuar snap.install.available_channels.help=Seleccione uno si desea continuar
snap.install.available_channels.message=No hay un canal {} ( stable ) disponible para {}. Pero hay estos otros abajo snap.install.available_channels.message=No hay un canal {} ( stable ) disponible para {}. Pero hay estos otros abajo

View File

@@ -2,15 +2,18 @@ gem.snap.info=Applicazioni pubblicate su https://snapcraft.io/store
snap.action.refresh.confirm=Ripristina {} ? snap.action.refresh.confirm=Ripristina {} ?
snap.action.refresh.label=Ripristina snap.action.refresh.label=Ripristina
snap.action.refresh.status=Ripristinare snap.action.refresh.status=Ripristinare
snap.info.channel=channel
snap.info.commands=commands snap.info.commands=commands
snap.info.contact=contact snap.info.contact=contact
snap.info.description=description snap.info.description=description
snap.info.download_size=Size (Download)
snap.info.installed=installed snap.info.installed=installed
snap.info.installed_size=Size (Instalattion)
snap.info.license=license snap.info.license=license
snap.info.publisher=publisher snap.info.publisher=publisher
snap.info.revision=revision snap.info.revision=revision
snap.info.size=size
snap.info.tracking=tracking snap.info.tracking=tracking
snap.info.type=type
snap.info.version=version snap.info.version=version
snap.install.available_channels.help=Selezionane uno se vuoi continuare snap.install.available_channels.help=Selezionane uno se vuoi continuare
snap.install.available_channels.message=Non esiste un {} canale disponibile per {}. Ma ci sono questi sotto snap.install.available_channels.message=Non esiste un {} canale disponibile per {}. Ma ci sono questi sotto

View File

@@ -2,15 +2,18 @@ gem.snap.info=Aplicativos publicados em https://snapcraft.io/store
snap.action.refresh.confirm=Atualizar {} ? snap.action.refresh.confirm=Atualizar {} ?
snap.action.refresh.label=Atualizar snap.action.refresh.label=Atualizar
snap.action.refresh.status=Atualizando snap.action.refresh.status=Atualizando
snap.info.channel=canal
snap.info.commands=comandos snap.info.commands=comandos
snap.info.contact=contato snap.info.contact=contato
snap.info.description=descrição snap.info.description=descrição
snap.info.download_size=Tamanho (Download)
snap.info.installed=instalado snap.info.installed=instalado
snap.info.installed_size=Tamanho (Instalação)
snap.info.license=licença snap.info.license=licença
snap.info.publisher=publicador snap.info.publisher=publicador
snap.info.revision=revisão snap.info.revision=revisão
snap.info.size=tamanho
snap.info.tracking=tracking snap.info.tracking=tracking
snap.info.type=tipo
snap.info.version=versão snap.info.version=versão
snap.install.available_channels.help=Selecione algum se quiser continuar snap.install.available_channels.help=Selecione algum se quiser continuar
snap.install.available_channels.message=Não há um canal {} ( stable ) disponível para {}. Porém existem estes outros abaixo snap.install.available_channels.message=Não há um canal {} ( stable ) disponível para {}. Porém existem estes outros abaixo

View File

@@ -2,15 +2,18 @@ gem.snap.info=Приложения, опубликованные на https://sn
snap.action.refresh.confirm=Обновить {} ? snap.action.refresh.confirm=Обновить {} ?
snap.action.refresh.label=Обновить snap.action.refresh.label=Обновить
snap.action.refresh.status=Обновляется snap.action.refresh.status=Обновляется
snap.info.channel=channel
snap.info.commands=Команды snap.info.commands=Команды
snap.info.contact=Контакт snap.info.contact=Контакт
snap.info.description=Описание snap.info.description=Описание
snap.info.download_size=Size (Download)
snap.info.installed=Размер установки snap.info.installed=Размер установки
snap.info.installed_size=Size (Instalattion)
snap.info.license=Лицензия snap.info.license=Лицензия
snap.info.publisher=Издатель snap.info.publisher=Издатель
snap.info.revision=Ревизия snap.info.revision=Ревизия
snap.info.size=Размер
snap.info.tracking=Отслеживание snap.info.tracking=Отслеживание
snap.info.type=type
snap.info.version=Версия snap.info.version=Версия
snap.install.available_channels.help=Выберите один, если вы хотите продолжить snap.install.available_channels.help=Выберите один, если вы хотите продолжить
snap.install.available_channels.message=Нет одного канала {}, доступного для {}. Но есть такие ниже snap.install.available_channels.message=Нет одного канала {}, доступного для {}. Но есть такие ниже

View File

@@ -2,15 +2,18 @@ gem.snap.info=https://snapcraft.io/store adresinde yayınlanan uygulamalar
snap.action.refresh.confirm=Yenile {} ? snap.action.refresh.confirm=Yenile {} ?
snap.action.refresh.label=Yenile snap.action.refresh.label=Yenile
snap.action.refresh.status=Yenileniyor snap.action.refresh.status=Yenileniyor
snap.info.channel=channel
snap.info.commands=komutlar snap.info.commands=komutlar
snap.info.contact=iletişim snap.info.contact=iletişim
snap.info.description=açıklama snap.info.description=açıklama
snap.info.download_size=Size (Download)
snap.info.installed=yüklü snap.info.installed=yüklü
snap.info.installed_size=Size (Instalattion)
snap.info.license=lisans snap.info.license=lisans
snap.info.publisher=yayıncı snap.info.publisher=yayıncı
snap.info.revision=revizyon snap.info.revision=revizyon
snap.info.size=boyut
snap.info.tracking=izleme snap.info.tracking=izleme
snap.info.type=type
snap.info.version=sürüm snap.info.version=sürüm
snap.install.available_channels.help=Devam etmek istiyorsanız birini seçin snap.install.available_channels.help=Devam etmek istiyorsanız birini seçin
snap.install.available_channels.message={} için hiçbir {} kanalı yok. Ama aşağıda bunlar var snap.install.available_channels.message={} için hiçbir {} kanalı yok. Ama aşağıda bunlar var

View File

@@ -1,174 +1,15 @@
import logging
import os import os
import re
import subprocess import subprocess
from io import StringIO from io import StringIO
from typing import List, Tuple, Set from typing import Tuple
from bauh.commons.system import run_cmd, new_subprocess, SimpleProcess from bauh.commons.system import run_cmd, SimpleProcess
from bauh.gems.snap.model import SnapApplication
BASE_CMD = 'snap' BASE_CMD = 'snap'
RE_SNAPD_STATUS = re.compile('\s+')
RE_SNAPD_SERVICES = re.compile(r'snapd\.\w+.+')
def is_installed(): def is_installed() -> bool:
res = run_cmd('which snap', print_error=False) return bool(run_cmd('which {}'.format(BASE_CMD), print_error=False))
return res and not res.strip().startswith('which ')
def is_snapd_running() -> bool:
output = run_cmd('systemctl list-units')
snapd_services = RE_SNAPD_SERVICES.findall(output)
socket, socket_running, service, service_running = False, False, False, False
if snapd_services:
for service_line in snapd_services:
line_split = RE_SNAPD_STATUS.split(service_line)
running = line_split[3] in {'listening', 'running'}
if line_split[0] == 'snapd.service':
service = True
service_running = running
elif line_split[0] == 'snapd.socket':
socket = True
socket_running = running
return socket and socket_running and (not service or service_running)
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
}
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:
val = info[1].strip()
if info[0] == 'installed':
val_split = [s for s in val.split(' ') if s]
data['version'] = val_split[0]
if len(val_split) > 2:
data['size'] = val_split[2]
else:
data[info[0]] = val
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].strip().replace('- ', '').split('\n') if commands else None
return data
def read_installed(info_path: str) -> 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))
info_out = new_subprocess(['cat', *[info_path.format(a['name']) for a in apps]]).stdout
idx = -1
for o in new_subprocess(['grep', '-E', '(summary|apps)', '--colour=never'], stdin=info_out).stdout:
if o:
line = o.decode()
if line.startswith('summary:'):
idx += 1
apps[idx]['summary'] = line.split(':')[1].strip()
else:
apps[idx]['apps_field'] = True
return apps
def get_app_info_path() -> str:
if os.path.exists('/snap'):
return '/snap/{}/current/meta/snap.yaml'
elif os.path.exists('/var/lib/snapd/snap'):
return '/var/lib/snapd/snap/{}/current/meta/snap.yaml'
else:
return None
def has_apps_field(name: str, info_path: str) -> bool:
info_out = new_subprocess(['cat', info_path.format(name)]).stdout
res = False
for o in new_subprocess(['grep', '-E', 'apps', '--colour=never'], stdin=info_out).stdout:
if o:
line = o.decode()
if line.startswith('apps:'):
res = True
return res
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) -> SimpleProcess: def uninstall_and_stream(app_name: str, root_password: str) -> SimpleProcess:
@@ -199,56 +40,16 @@ def refresh_and_stream(app_name: str, root_password: str) -> SimpleProcess:
shell=True) shell=True)
def run(app: SnapApplication, logger: logging.Logger): def run(cmd: str):
info = get_info(app.name, 'commands') subprocess.Popen(['snap run {}'.format(cmd)], shell=True, env={**os.environ})
app_name = app.name.lower()
if info.get('commands'):
logger.info('Available commands found for {}: {}'.format(app_name, info['commands']))
commands = [c.strip() for c in info['commands']]
# trying to find an exact match command:
command = None
for c in commands:
if c.lower() == app_name:
command = c
logger.info("Found exact match command for '{}'".format(app_name))
break
if not command:
for c in commands:
if not c.endswith('.apm'):
command = c
if command:
logger.info("Running '{}'".format(command))
subprocess.Popen(['{} run {}'.format(BASE_CMD, command)], shell=True, env={**os.environ})
return
logger.error("No valid command found for '{}'".format(app_name))
else:
logger.error("No command found for '{}'".format(app_name))
def is_api_available() -> Tuple[bool, str]: def is_api_available() -> Tuple[bool, str]:
output = StringIO() output = StringIO()
for o in SimpleProcess(['snap', 'search']).instance.stdout: for o in SimpleProcess([BASE_CMD, 'search']).instance.stdout:
if o: if o:
output.write(o.decode()) output.write(o.decode())
output.seek(0) output.seek(0)
output = output.read() output = output.read()
return 'error:' not in output, output return 'error:' not in output, output
def list_installed_names() -> Set[str]:
res = run_cmd('{} list'.format(BASE_CMD), print_error=False)
if res:
lines = res.split('\n')
if not lines[0].startswith('error'):
return {l.split(' ')[0].strip() for l in lines[1:] if l}

136
bauh/gems/snap/snapd.py Normal file
View File

@@ -0,0 +1,136 @@
import re
import socket
import traceback
from logging import Logger
from typing import Optional, List
from requests import Session
from requests.adapters import HTTPAdapter
from urllib3.connection import HTTPConnection
from urllib3.connectionpool import HTTPConnectionPool
from bauh.commons.system import run_cmd
URL_BASE = 'http://snapd/v2'
RE_SNAPD_STATUS = re.compile('\s+')
RE_SNAPD_SERVICES = re.compile(r'snapd\.\w+.+')
class SnapdConnection(HTTPConnection):
def __init__(self):
super(SnapdConnection, self).__init__('localhost')
def connect(self):
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.sock.connect("/run/snapd.socket")
class SnapdConnectionPool(HTTPConnectionPool):
def __init__(self):
super(SnapdConnectionPool, self).__init__('localhost')
def _new_conn(self):
return SnapdConnection()
class SnapdAdapter(HTTPAdapter):
def get_connection(self, url, proxies=None):
return SnapdConnectionPool()
class SnapdClient:
def __init__(self, logger: Logger):
self.logger = logger
self.session = self._new_session()
def _new_session(self) -> Optional[Session]:
try:
session = Session()
session.mount("http://snapd/", SnapdAdapter())
return session
except:
self.logger.error("Could not establish a connection to 'snapd.socker'")
traceback.print_exc()
def query(self, query: str) -> Optional[List[dict]]:
final_query = query.strip()
if final_query and self.session:
res = self.session.get(url='{}/find'.format(URL_BASE), params={'q': final_query})
if res.status_code == 200:
json_res = res.json()
if json_res['status-code'] == 200:
return json_res['result']
def find_by_name(self, name: str) -> Optional[List[dict]]:
if name and self.session:
res = self.session.get('{}/find?name={}'.format(URL_BASE, name))
if res.status_code == 200:
json_res = res.json()
if json_res['status-code'] == 200:
return json_res['result']
def list_all_snaps(self) -> List[dict]:
if self.session:
res = self.session.get('{}/snaps'.format(URL_BASE))
if res.status_code == 200:
json_res = res.json()
if json_res['status-code'] == 200:
return json_res['result']
return []
def list_only_apps(self) -> List[dict]:
if self.session:
res = self.session.get('{}/apps'.format(URL_BASE))
if res.status_code == 200:
json_res = res.json()
if json_res['status-code'] == 200:
return json_res['result']
return []
def list_commands(self, name: str) -> List[dict]:
if self.session:
res = self.session.get('{}/apps?names={}'.format(URL_BASE, name))
if res.status_code == 200:
json_res = res.json()
if json_res['status-code'] == 200:
return [r for r in json_res['result'] if r['snap'] == name]
return []
def is_running() -> bool:
output = run_cmd('systemctl list-units', print_error=False)
if not output:
return False
snapd_services = RE_SNAPD_SERVICES.findall(output)
snap_socket, socket_running, service, service_running = False, False, False, False
if snapd_services:
for service_line in snapd_services:
line_split = RE_SNAPD_STATUS.split(service_line)
running = line_split[3] in {'listening', 'running'}
if line_split[0] == 'snapd.service':
service = True
service_running = running
elif line_split[0] == 'snapd.socket':
snap_socket = True
socket_running = running
return snap_socket and socket_running and (not service or service_running)

View File

@@ -1,70 +0,0 @@
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)