gem selector sketch

This commit is contained in:
Vinicius Moreira
2019-09-11 16:00:02 -03:00
parent 696b6169ac
commit ba5e1da11b
79 changed files with 4117 additions and 112 deletions

0
bauh/gems/__init__.py Normal file
View File

View File

@@ -0,0 +1,4 @@
import os
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
BUILD_DIR = '/tmp/bauh/aur'

28
bauh/gems/arch/aur.py Normal file
View File

@@ -0,0 +1,28 @@
import re
from typing import Set, List
from bauh.api.http import HttpClient
URL_INFO = 'https://aur.archlinux.org/rpc/?v=5&type=info&'
URL_SEARCH = 'https://aur.archlinux.org/rpc/?v=5&type=search&arg='
def map_pkgbuild(pkgbuild: str) -> dict:
return {attr: val.replace('"', '').replace("'", '').replace('(', '').replace(')', '') for attr, val in re.findall(r'\n(\w+)=(.+)', pkgbuild)}
class AURClient:
def __init__(self, http_client: HttpClient):
self.http_client = http_client
self.names_index = set()
def search(self, words: str) -> dict:
return self.http_client.get_json(URL_SEARCH + words)
def get_info(self, names: Set[str]) -> List[dict]:
res = self.http_client.get_json(URL_INFO + self._map_names_as_queries(names))
return res['results'] if res and res.get('results') else []
def _map_names_as_queries(self, names) -> str:
return '&'.join(['arg[{}]={}'.format(i, n) for i, n in enumerate(names)])

View File

@@ -0,0 +1,28 @@
from typing import Set
from bauh.api.abstract.handler import ProcessWatcher
from bauh.api.abstract.view import MultipleSelectComponent, InputOption
from bauh.commons.html import bold
def request_optional_deps(pkgname: str, pkg_mirrors: dict, watcher: ProcessWatcher, i18n: dict) -> Set[str]:
view_opts = MultipleSelectComponent(label='',
options=[InputOption('{} ( {} )'.format(p, m.upper()), p) for p, m in
pkg_mirrors.items()])
install = watcher.request_confirmation(title=i18n['arch.install.optdeps.request.title'],
body='<p>{}</p>'.format(i18n['arch.install.optdeps.request.body'].format(bold(pkgname)) + ':'),
components=[view_opts],
confirmation_label=i18n['install'],
deny_label=i18n['cancel'])
if install:
return {o.value for o in view_opts.values}
def request_install_missing_deps(pkgname: str, pkg_mirrors: dict, watcher: ProcessWatcher, i18n: dict) -> bool:
deps_str = ''.join(['<br/><span style="font-weight:bold"> - {} ( {} )</span>'.format(d, m.upper()) for d, m in pkg_mirrors.items()])
msg = '<p>{}</p>'.format(i18n['arch.missing_deps.body'].format('<span style="font-weight: bold">{}</span>'.format(pkgname) + ':<br/>' + deps_str))
msg += i18n['ask.continue']
return watcher.request_confirmation(i18n['arch.missing_deps.title'], msg)

View File

@@ -0,0 +1,570 @@
import os
import re
import shutil
import time
from pathlib import Path
from threading import Thread
from typing import List, Set, Type
from bauh.api.abstract.controller import SearchResult, SoftwareManager, ApplicationContext
from bauh.api.abstract.disk import DiskCacheLoader
from bauh.api.abstract.handler import ProcessWatcher
from bauh.api.abstract.model import PackageUpdate, PackageHistory, SoftwarePackage, PackageSuggestion
from bauh.api.abstract.view import MessageType
from bauh.commons.system import SystemProcess, ProcessHandler, new_subprocess, run_cmd, new_root_subprocess
from bauh.gems.arch import BUILD_DIR, aur, pacman, makepkg, pkgbuild, message, confirmation, disk, git, suggestions
from bauh.gems.arch.aur import AURClient
from bauh.gems.arch.mapper import ArchDataMapper
from bauh.gems.arch.model import ArchPackage
from bauh.commons.html import bold
from bauh.gems.arch.worker import AURIndexUpdater, ArchDiskCacheUpdater
URL_GIT = 'https://aur.archlinux.org/{}.git'
URL_PKG_DOWNLOAD = 'https://aur.archlinux.org/cgit/aur.git/snapshot/{}.tar.gz'
URL_SRC_INFO = 'https://aur.archlinux.org/cgit/aur.git/plain/.SRCINFO?h='
RE_SPLIT_VERSION = re.compile(r'(=|>|<)')
class ArchManager(SoftwareManager):
def __init__(self, context: ApplicationContext):
super(ArchManager, self).__init__(context=context)
self.aur_cache = context.cache_factory.new()
# context.disk_loader_factory.map(ArchPackage, self.aur_cache) TODO
self.mapper = ArchDataMapper(http_client=context.http_client)
self.i18n = context.i18n
self.aur_client = AURClient(context.http_client)
self.names_index = {}
self.aur_index_updater = AURIndexUpdater(context, self)
self.dcache_updater = ArchDiskCacheUpdater(context.logger)
self.logger = context.logger
def _upgrade_search_result(self, apidata: dict, installed_pkgs: dict, downgrade_enabled: bool, res: SearchResult, disk_loader: DiskCacheLoader):
app = self.mapper.map_api_data(apidata, installed_pkgs['not_signed'])
app.downgrade_enabled = downgrade_enabled
if app.installed:
res.installed.append(app)
if disk_loader:
disk_loader.fill(app)
else:
res.new.append(app)
Thread(target=self.mapper.fill_package_build, args=(app,)).start()
def search(self, words: str, disk_loader: DiskCacheLoader, limit: int = -1) -> SearchResult:
downgrade_enabled = git.is_enabled()
res = SearchResult([], [], 0)
installed = {}
read_installed = Thread(target=lambda: installed.update(pacman.list_and_map_installed()))
read_installed.start()
api_res = self.aur_client.search(words)
if api_res and api_res.get('results'):
read_installed.join()
for pkgdata in api_res['results']:
self._upgrade_search_result(pkgdata, installed, downgrade_enabled, res, disk_loader)
else: # if there are no results from the API (it could be because there were too many), tries the names index:
if self.names_index:
to_query = set()
for norm_name, real_name in self.names_index.items():
if words in norm_name:
to_query.add(real_name)
if len(to_query) == 25:
break
pkgsinfo = self.aur_client.get_info(to_query)
if pkgsinfo:
read_installed.join()
for pkgdata in pkgsinfo:
self._upgrade_search_result(pkgdata, installed, res)
res.total = len(res.installed) + len(res.new)
return res
def _fill_aur_pkgs(self, not_signed: dict, apps: list, disk_loader: DiskCacheLoader):
downgrade_enabled = git.is_enabled()
pkgsinfo = self.aur_client.get_info(not_signed.keys())
if pkgsinfo:
for pkgdata in pkgsinfo:
app = self.mapper.map_api_data(pkgdata, not_signed)
app.downgrade_enabled = downgrade_enabled
if disk_loader:
disk_loader.fill(app)
apps.append(app)
def _fill_mirror_pkgs(self, mirrors: dict, apps: list):
# TODO
for name, data in mirrors.items():
app = ArchPackage(name=name, version=data.get('version'), latest_version=data.get('version'), description=data.get('description'))
app.installed = True
app.mirror = '' # TODO
app.update = False # TODO
apps.append(app)
def read_installed(self, disk_loader: DiskCacheLoader, limit: int = -1, only_apps: bool = False, pkg_types: Set[Type[SoftwarePackage]] = None) -> SearchResult:
installed = pacman.list_and_map_installed()
apps = []
if installed and installed['not_signed']:
self._fill_aur_pkgs(installed['not_signed'], apps, disk_loader)
return SearchResult(apps, None, len(apps))
def downgrade(self, pkg: ArchPackage, root_password: str, watcher: ProcessWatcher) -> bool:
handler = ProcessHandler(watcher)
app_build_dir = '{}/build_{}'.format(BUILD_DIR, int(time.time()))
watcher.change_progress(5)
try:
if not os.path.exists(app_build_dir):
build_dir = handler.handle(SystemProcess(new_subprocess(['mkdir', '-p', app_build_dir])))
if build_dir:
watcher.change_progress(10)
watcher.change_substatus(self.i18n['arch.clone'].format(bold(pkg.name)))
clone = handler.handle(SystemProcess(subproc=new_subprocess(['git', 'clone', URL_GIT.format(pkg.name)], cwd=app_build_dir), check_error_output=False))
watcher.change_progress(30)
if clone:
watcher.change_substatus(self.i18n['arch.downgrade.reading_commits'])
clone_path = '{}/{}'.format(app_build_dir, pkg.name)
pkgbuild_path = '{}/PKGBUILD'.format(clone_path)
commits = run_cmd("git log", cwd=clone_path)
watcher.change_progress(40)
if commits:
commit_list = re.findall(r'commit (.+)\n', commits)
if commit_list:
if len(commit_list) > 1:
for idx in range(1, len(commit_list)):
commit = commit_list[idx]
with open(pkgbuild_path) as f:
pkgdict = aur.map_pkgbuild(f.read())
if not handler.handle(SystemProcess(subproc=new_subprocess(['git', 'reset', '--hard', commit], cwd=clone_path), check_error_output=False)):
watcher.print('Could not downgrade anymore. Aborting...')
return False
if '{}-{}'.format(pkgdict.get('pkgver'), pkgdict.get('pkgrel')) == pkg.version:
# current version found
watcher.change_substatus(self.i18n['arch.downgrade.version_found'])
break
watcher.change_substatus(self.i18n['arch.downgrade.install_older'])
return self._make_pkg(pkg.name, root_password, handler, app_build_dir, clone_path, dependency=False, skip_optdeps=True)
else:
watcher.show_message(title=self.i18n['arch.downgrade.error'],
body=self.i18n['arch.downgrade.impossible'].format(pkg.name),
type_=MessageType.ERROR)
return False
watcher.show_message(title=self.i18n['error'], body=self.i18n['arch.downgrade.no_commits'], type_=MessageType.ERROR)
return False
finally:
if os.path.exists(app_build_dir):
handler.handle(SystemProcess(subproc=new_subprocess(['rm', '-rf', app_build_dir])))
return False
def clean_cache_for(self, pkg: ArchPackage):
if os.path.exists(pkg.get_disk_cache_path()):
shutil.rmtree(pkg.get_disk_cache_path())
def update(self, pkg: ArchPackage, root_password: str, watcher: ProcessWatcher) -> bool:
return self.install(pkg=pkg, root_password=root_password, watcher=watcher, skip_optdeps=True)
def _uninstall(self, pkg_name: str, root_password: str, handler: ProcessHandler) -> bool:
return handler.handle(SystemProcess(new_root_subprocess(['pacman', '-R', pkg_name, '--noconfirm'], root_password)))
def uninstall(self, pkg: ArchPackage, root_password: str, watcher: ProcessWatcher) -> bool:
handler = ProcessHandler(watcher)
watcher.change_progress(10)
info = pacman.get_info_dict(pkg.name)
watcher.change_progress(50)
if info.get('required by'):
pkname = '"{}"'.format(pkg.name)
msg = '{}:\n\n{}\n\n{}'.format(self.i18n['arch.uninstall.required_by'].format(pkname), info['required by'], self.i18n['arch.uninstall.required_by.advice'].format(pkname))
watcher.show_message(title=self.i18n['error'], body=msg, type_=MessageType.WARNING)
return False
uninstalled = self._uninstall(pkg.name, root_password, handler)
watcher.change_progress(100)
return uninstalled
def get_managed_types(self) -> Set["type"]:
return {ArchPackage}
def get_info(self, pkg: ArchPackage) -> dict:
if pkg.installed:
t = Thread(target=self.mapper.fill_package_build, args=(pkg,))
t.start()
info = pacman.get_info_dict(pkg.name)
t.join()
if pkg.pkgbuild:
info['13_pkg_build'] = pkg.pkgbuild
return info
else:
info = {
'01_id': pkg.id,
'02_name': pkg.name,
'03_version': pkg.version,
'04_popularity': pkg.popularity,
'05_votes': pkg.votes,
'06_package_base': pkg.package_base,
'07_maintainer': pkg.maintainer,
'08_first_submitted': pkg.first_submitted,
'09_last_modified': pkg.last_modified,
'10_url': pkg.url_download,
}
res_srcinfo = self.context.http_client.get(URL_SRC_INFO + pkg.name)
if res_srcinfo and res_srcinfo.text:
info['11_dependson'] = ' '.join(pkgbuild.read_depends_on(res_srcinfo.text))
info['12_optdepends'] = ' '.join(pkgbuild.read_optdeps(res_srcinfo.text))
if pkg.pkgbuild:
info['13_pkg_build'] = pkg.pkgbuild
else:
info['11_pkg_build_url'] = pkg.get_pkg_build_url()
return info
def get_history(self, pkg: ArchPackage) -> PackageHistory:
temp_dir = '{}/build_{}'.format(BUILD_DIR, int(time.time()))
try:
Path(temp_dir).mkdir(parents=True)
run_cmd('git clone ' + URL_GIT.format(pkg.name), print_error=False, cwd=temp_dir)
clone_path = '{}/{}'.format(temp_dir, pkg.name)
pkgbuild_path = '{}/PKGBUILD'.format(clone_path)
commits = git.list_commits(clone_path)
if commits:
history, status_idx = [], 0
for idx, commit in enumerate(commits):
with open(pkgbuild_path) as f:
pkgdict = aur.map_pkgbuild(f.read())
if '{}-{}'.format(pkgdict.get('pkgver'), pkgdict.get('pkgrel')) == pkg.version:
status_idx = idx
history.append({'1_version': pkgdict['pkgver'], '2_release': pkgdict['pkgrel'],
'3_date': commit['date']}) # the number prefix is to ensure the rendering order
if idx + 1 < len(commits):
if not run_cmd('git reset --hard ' + commits[idx + 1]['commit'], cwd=clone_path):
break
return PackageHistory(pkg=pkg, history=history, pkg_status_idx=status_idx)
finally:
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir)
def _install_deps(self, deps: Set[str], pkg_mirrors: dict, root_password: str, handler: ProcessHandler, change_progress: bool = False) -> str:
"""
:param deps:
:param pkg_mirrors:
:param root_password:
:param handler:
:return: not installed dependency
"""
progress_increment = int(100 / len(deps))
progress = 0
self._update_progress(handler.watcher, 1, change_progress)
for pkgname in deps:
mirror = pkg_mirrors[pkgname]
handler.watcher.change_substatus(self.i18n['arch.install.dependency.install'].format(bold('{} ()'.format(pkgname, mirror))))
if mirror == 'aur':
installed = self._install_from_aur(pkgname=pkgname, root_password=root_password, handler=handler, dependency=True, change_progress=False)
else:
installed = self._install(pkgname=pkgname, root_password=root_password, handler=handler, install_file=None, mirror=mirror, change_progress=False)
if not installed:
return pkgname
progress += progress_increment
self._update_progress(handler.watcher, progress, change_progress)
self._update_progress(handler.watcher, 100, change_progress)
def _map_mirrors(self, pkgnames: Set[str]) -> dict:
pkg_mirrors = pacman.get_mirrors(pkgnames) # getting mirrors set
if len(pkgnames) != pkg_mirrors: # checking if any dep not found in the distro mirrors are from AUR
nomirrors = {p for p in pkgnames if p not in pkg_mirrors}
for pkginfo in self.aur_client.get_info(nomirrors):
if pkginfo.get('Name') in nomirrors:
pkg_mirrors[pkginfo['Name']] = 'aur'
return pkg_mirrors
def _make_pkg(self, pkgname: str, root_password: str, handler: ProcessHandler, build_dir: str, project_dir: str, dependency: bool, skip_optdeps: bool = False, change_progress: bool = True) -> bool:
self._update_progress(handler.watcher, 50, change_progress)
if not self._install_missings_deps(pkgname, root_password, handler, project_dir):
return False
# building main package
handler.watcher.change_substatus(self.i18n['arch.building.package'].format(bold(pkgname)))
pkgbuilt = handler.handle(SystemProcess(new_subprocess(['makepkg', '-Acsmf'], cwd=project_dir)))
self._update_progress(handler.watcher, 65, change_progress)
if pkgbuilt:
gen_file = [fname for root, dirs, files in os.walk(build_dir) for fname in files if re.match(r'^{}-.+\.tar\.xz'.format(pkgname), fname)]
if not gen_file:
handler.watcher.print('Could not find generated .tar.xz file. Aborting...')
return False
install_file = '{}/{}'.format(project_dir, gen_file[0])
if self._install(pkgname=pkgname, root_password=root_password, mirror='aur', handler=handler, install_file=install_file, pkgdir=project_dir, change_progress=change_progress):
if dependency or skip_optdeps:
return True
handler.watcher.change_substatus(self.i18n['arch.optdeps.checking'].format(bold(pkgname)))
if self._install_optdeps(pkgname, root_password, handler, project_dir, change_progress=change_progress):
return True
return False
def _install_missings_deps(self, pkgname: str, root_password: str, handler: ProcessHandler, pkgdir: str) -> bool:
handler.watcher.change_substatus(self.i18n['arch.checking.deps'].format(bold(pkgname)))
missing_deps = makepkg.check_missing_deps(pkgdir, handler.watcher)
if missing_deps:
depnames = {RE_SPLIT_VERSION.split(dep)[0] for dep in missing_deps}
dep_mirrors = self._map_mirrors(depnames)
for dep in depnames: # cheking if a dependency could not be found in any mirror
if dep not in dep_mirrors:
message.show_dep_not_found(dep, self.i18n, handler.watcher)
return False
handler.watcher.change_substatus(self.i18n['arch.missing_deps_found'].format(bold(pkgname)))
if not confirmation.request_install_missing_deps(pkgname, dep_mirrors, handler.watcher, self.i18n):
handler.watcher.print(self.i18n['action.cancelled'])
return False
dep_not_installed = self._install_deps(depnames, dep_mirrors, root_password, handler, change_progress=False)
if dep_not_installed:
message.show_dep_not_installed(handler.watcher, pkgname, dep_not_installed, self.i18n)
return False
return True
def _install_optdeps(self, pkgname: str, root_password: str, handler: ProcessHandler, pkgdir: str, change_progress: bool = True) -> bool:
with open('{}/.SRCINFO'.format(pkgdir)) as f:
odeps = pkgbuild.read_optdeps(f.read())
if not odeps:
return True
installed = pacman.list_installed()
to_install = {d for d in odeps if d not in installed}
if not to_install:
return True
pkg_mirrors = self._map_mirrors(to_install)
if pkg_mirrors:
deps_to_install = confirmation.request_optional_deps(pkgname, pkg_mirrors, handler.watcher, self.i18n)
if not deps_to_install:
return True
else:
dep_not_installed = self._install_deps(deps_to_install, pkg_mirrors, root_password, handler, change_progress=True)
if dep_not_installed:
message.show_optdep_not_installed(dep_not_installed, handler.watcher, self.i18n)
return False
return True
def _install(self, pkgname: str, root_password: str, mirror: str, handler: ProcessHandler, install_file: str = None, pkgdir: str = '.', change_progress: bool = True):
check_install_output = []
pkgpath = install_file if install_file else pkgname
handler.watcher.change_substatus(self.i18n['arch.checking.conflicts'].format(bold(pkgname)))
for check_out in new_root_subprocess(['pacman', '-U', pkgpath], root_password=root_password, cwd=pkgdir).stdout:
if check_out:
check_install_output.append(check_out.decode())
self._update_progress(handler.watcher, 70, change_progress)
if check_install_output and 'conflict' in check_install_output[-1]:
conflicting_apps = [w[0] for w in re.findall(r'((\w|\-|\.)+)\s(and|are)', check_install_output[-1])]
conflict_msg = ' {} '.format(self.i18n['and']).join(conflicting_apps)
if not handler.watcher.request_confirmation(title=self.i18n['arch.install.conflict.popup.title'],
body=self.i18n['arch.install.conflict.popup.body'].format(conflict_msg)):
handler.watcher.print(self.i18n['action.cancelled'])
return False
else: # uninstall conflicts
self._update_progress(handler.watcher, 75, change_progress)
to_uninstall = [conflict for conflict in conflicting_apps if conflict != pkgname]
for conflict in to_uninstall:
handler.watcher.change_substatus(self.i18n['arch.uninstalling.conflict'].format(bold(conflict)))
if not self._uninstall(conflict, root_password, handler):
handler.watcher.show_message(title=self.i18n['error'],
body=self.i18n['arch.uninstalling.conflict.fail'].format('"{}"'.format(conflict)),
type_=MessageType.ERROR)
return False
handler.watcher.change_substatus(self.i18n['arch.installing.package'].format(bold(pkgname)))
self._update_progress(handler.watcher, 80, change_progress)
installed = handler.handle(pacman.install_as_process(pkgpath=pkgpath, root_password=root_password, aur=install_file is not None, pkgdir=pkgdir))
self._update_progress(handler.watcher, 95, change_progress)
if installed and self.context.disk_cache:
handler.watcher.change_substatus(self.i18n['status.caching_data'].format(bold(pkgname)))
disk.save(pkgname, mirror)
self._update_progress(handler.watcher, 100, change_progress)
return installed
def _update_progress(self, watcher: ProcessWatcher, val: int, change_progress: bool):
if change_progress:
watcher.change_progress(val)
def _install_from_aur(self, pkgname: str, root_password: str, handler: ProcessHandler, dependency: bool, skip_optdeps: bool = False, change_progress: bool = True) -> bool:
app_build_dir = '{}/build_{}'.format(BUILD_DIR, int(time.time()))
try:
if not os.path.exists(app_build_dir):
build_dir = handler.handle(SystemProcess(new_subprocess(['mkdir', '-p', app_build_dir])))
self._update_progress(handler.watcher, 10, change_progress)
if build_dir:
file_url = URL_PKG_DOWNLOAD.format(pkgname)
file_name = file_url.split('/')[-1]
handler.watcher.change_substatus('{} {}'.format(self.i18n['arch.downloading.package'], bold(file_name)))
download = handler.handle(SystemProcess(new_subprocess(['wget', file_url], cwd=app_build_dir)))
if download:
self._update_progress(handler.watcher, 30, change_progress)
handler.watcher.change_substatus('{} {}'.format(self.i18n['arch.uncompressing.package'], bold(file_name)))
uncompress = handler.handle(SystemProcess(new_subprocess(['tar', 'xvzf', '{}.tar.gz'.format(pkgname)], cwd=app_build_dir)))
self._update_progress(handler.watcher, 40, change_progress)
if uncompress:
uncompress_dir = '{}/{}'.format(app_build_dir, pkgname)
return self._make_pkg(pkgname=pkgname,
root_password=root_password,
handler=handler,
build_dir=app_build_dir,
project_dir=uncompress_dir,
dependency=dependency,
skip_optdeps=skip_optdeps,
change_progress=change_progress)
finally:
if os.path.exists(app_build_dir):
handler.handle(SystemProcess(new_subprocess(['rm', '-rf', app_build_dir])))
return False
def install(self, pkg: ArchPackage, root_password: str, watcher: ProcessWatcher, skip_optdeps: bool = False) -> bool:
return self._install_from_aur(pkg.name, root_password, ProcessHandler(watcher), dependency=False, skip_optdeps=skip_optdeps)
def _is_wget_available(self):
try:
new_subprocess(['wget', '--version'])
return True
except FileNotFoundError:
return False
def is_enabled(self) -> bool:
try:
return pacman.is_enabled() and self._is_wget_available()
except FileNotFoundError:
return False
def is_downgrade_enabled(self) -> bool:
try:
new_subprocess(['git', '--version'])
return True
except FileNotFoundError:
return False
def cache_to_disk(self, pkg: ArchPackage, icon_bytes: bytes, only_icon: bool):
if self.context.disk_cache and pkg.supports_disk_cache():
pass
def requires_root(self, action: str, pkg: ArchPackage):
return action != 'search'
def prepare(self):
self.dcache_updater.start()
self.aur_index_updater.start()
def list_updates(self) -> List[PackageUpdate]:
return [PackageUpdate(app.id, app.latest_version, 'aur') for app in self.read_installed(disk_loader=None).installed if app.update]
def list_warnings(self) -> List[str]:
warnings = []
if not pacman.is_enabled():
warnings.append(self.i18n['arch.warning.disabled'].format(bold('pacman')))
if not self._is_wget_available():
warnings.append(self.i18n['arch.warning.disabled'].format(bold('wget')))
if not git.is_enabled():
warnings.append(self.i18n['arch.warning.git'].format(bold('git')))
return warnings
def list_suggestions(self, limit: int) -> List[PackageSuggestion]:
res = []
sugs = [(i, p) for i, p in suggestions.ALL.items()]
sugs.sort(key=lambda t: t[1].value, reverse=True)
if limit > 0:
sugs = sugs[0:limit]
sug_names = {s[0] for s in sugs}
api_res = self.aur_client.get_info(sug_names)
if api_res:
for pkg in api_res:
if pkg.get('Name') in sug_names:
res.append(PackageSuggestion(self.mapper.map_api_data(pkg, {}), suggestions.ALL.get(pkg['Name'])))
return res

179
bauh/gems/arch/disk.py Normal file
View File

@@ -0,0 +1,179 @@
import json
import os
import re
from pathlib import Path
from typing import Set, List
from bauh.gems.arch import pacman
from bauh.gems.arch.model import ArchPackage
RE_DESKTOP_ENTRY = re.compile(r'(Exec|Icon)\s*=\s*(.+)')
RE_CLEAN_NAME = re.compile(r'^(\w+)-?|_?.+')
def write(app: ArchPackage):
Path(app.get_disk_cache_path()).mkdir(parents=True, exist_ok=True)
with open(app.get_disk_data_path(), 'w+') as f:
f.write(json.dumps(app.get_data_to_cache()))
def fill_icon_path(app: ArchPackage, icon_paths: List[str], only_exact_match: bool):
ends_with = re.compile(r'.+/{}\.(png|svg)$'.format(app.name), re.IGNORECASE)
for path in icon_paths:
if ends_with.match(path):
app.icon_path = path
return
if not only_exact_match:
pkg_icons_path = pacman.list_icon_paths({app.name})
if pkg_icons_path:
app.icon_path = pkg_icons_path[0]
def set_icon_path(app: ArchPackage, icon_name: str = None):
installed_icons = pacman.list_icon_paths(app.name)
if installed_icons:
exact_match = re.compile(r'.+/{}\..+$'.format(icon_name.split('.')[0] if icon_name else app.name))
for icon_path in installed_icons:
if exact_match.match(icon_path):
app.icon_path = icon_path
break
def save(pkgname: str, mirror: str):
app = ArchPackage(name=pkgname, mirror=mirror)
desktop_files = pacman.list_desktop_entries({pkgname})
if desktop_files:
if len(desktop_files) > 1:
exact_match = [f for f in desktop_files if f.endswith('{}.desktop'.format(pkgname))]
to_parse = exact_match[0] if exact_match else desktop_files[0]
else:
to_parse = desktop_files[0]
app.desktop_entry = to_parse
with open(to_parse) as f:
desktop_entry = f.read()
for field in RE_DESKTOP_ENTRY.findall(desktop_entry):
if field[0] == 'Exec':
app.command = field[1].strip()
elif field[0] == 'Icon':
app.icon_path = field[1].strip()
if app.icon_path and '/' not in app.icon_path: # if the icon full path is not defined
set_icon_path(app, app.icon_path)
else:
bin_paths = pacman.list_bin_paths({pkgname})
if bin_paths:
if len(bin_paths) > 1:
exact_match = [p for p in bin_paths if p.endswith('/' + pkgname)]
app.command = exact_match[0] if exact_match else bin_paths[0]
else:
app.command = bin_paths[0]
set_icon_path(app)
Path(app.get_disk_cache_path()).mkdir(parents=True, exist_ok=True)
with open(app.get_disk_data_path(), 'w+') as f:
f.write(json.dumps(app.get_data_to_cache()))
def save_several(pkgnames: Set[str], mirror: str, overwrite: bool = True) -> int:
to_cache = {n for n in pkgnames if overwrite or not os.path.exists(ArchPackage.disk_cache_path(n, mirror))}
desktop_files = pacman.list_desktop_entries(to_cache)
no_desktop_files = {}
to_write = []
if desktop_files:
desktop_matches, no_exact_match = {}, set()
for pkg in to_cache: # first try to find exact matches
ends_with = re.compile('.+/{}.desktop$'.format(pkg), re.IGNORECASE)
for f in desktop_files:
if ends_with.match(f):
desktop_matches[pkg] = f
break
if pkg not in desktop_matches:
no_exact_match.add(pkg)
if no_exact_match: # check every not matched app individually
for pkg in no_exact_match:
entries = pacman.list_desktop_entries({pkg})
if entries:
desktop_matches[pkg] = entries[0]
if not desktop_matches:
no_desktop_files = to_cache
else:
if len(desktop_matches) != len(to_cache):
no_desktop_files = {p for p in to_cache if p not in desktop_matches}
pkgs, apps_icons_noabspath = [], []
for pkgname, file in desktop_matches.items():
p = ArchPackage(name=pkgname, mirror=mirror)
p.desktop_entry = file
with open(file) as f:
desktop_entry = f.read()
for field in RE_DESKTOP_ENTRY.findall(desktop_entry):
if field[0] == 'Exec':
p.command = field[1].strip()
elif field[0] == 'Icon':
p.icon_path = field[1].strip()
if p.icon_path and '/' not in p.icon_path: # if the icon full path is not defined
apps_icons_noabspath.append(p)
pkgs.append(p)
if apps_icons_noabspath:
icon_paths = pacman.list_icon_paths({app.name for app in apps_icons_noabspath})
if icon_paths:
for p in apps_icons_noabspath:
fill_icon_path(p, icon_paths, False)
for p in pkgs:
to_write.append(p)
if no_desktop_files:
pkgs = {ArchPackage(name=n, mirror=mirror) for n in no_desktop_files}
bin_paths = pacman.list_bin_paths(no_desktop_files)
if bin_paths:
for p in pkgs:
ends_with = re.compile(r'.+/{}$'.format(p.name), re.IGNORECASE)
for path in bin_paths:
if ends_with.match(path):
p.command = path
break
icon_paths = pacman.list_icon_paths(no_desktop_files)
if icon_paths:
for p in pkgs:
fill_icon_path(p, icon_paths, only_exact_match=True)
for p in pkgs:
to_write.append(p)
if to_write:
for p in to_write:
write(p)
return len(to_write)
return 0

29
bauh/gems/arch/git.py Normal file
View File

@@ -0,0 +1,29 @@
from datetime import datetime
from typing import List
from bauh.commons.system import new_subprocess
def is_enabled() -> bool:
try:
new_subprocess(['git', '--version'])
return True
except FileNotFoundError:
return False
def list_commits(proj_dir:str) -> List[dict]:
logs = new_subprocess(['git', 'log', '--date=iso'], cwd=proj_dir).stdout
commits, commit = [], {}
for out in new_subprocess(['grep', '-E', 'commit|Date:'], stdin=logs).stdout:
if out:
line = out.decode()
if line.startswith('commit'):
commit['commit'] = line.split(' ')[1].strip()
elif line.startswith('Date'):
commit['date'] = datetime.fromisoformat(line.split(':')[1].strip())
commits.append(commit)
commit = {}
return commits

28
bauh/gems/arch/makepkg.py Normal file
View File

@@ -0,0 +1,28 @@
import re
from typing import List
from bauh.api.abstract.handler import ProcessWatcher
from bauh.commons.system import new_subprocess
RE_DEPS_PATTERN = re.compile(r'\n?\s+->\s(.+)\n')
def check_missing_deps(pkgdir: str, watcher: ProcessWatcher) -> List[str]:
depcheck = new_subprocess(['makepkg', '--check'], cwd=pkgdir)
for o in depcheck.stdout:
if o:
watcher.print(o.decode())
error_lines = []
for s in depcheck.stderr:
if s:
line = s.decode()
print(line)
error_lines.append(line)
if error_lines:
error_str = ''.join(error_lines)
if 'Missing dependencies' in error_str:
return RE_DEPS_PATTERN.findall(error_str)

55
bauh/gems/arch/mapper.py Normal file
View File

@@ -0,0 +1,55 @@
from datetime import datetime
from bauh.api.http import HttpClient
from bauh.gems.arch.model import ArchPackage
URL_PKG_DOWNLOAD = 'https://aur.archlinux.org/{}'
class ArchDataMapper:
def __init__(self, http_client: HttpClient):
self.http_client = http_client
def fill_api_data(self, pkg: ArchPackage, package: dict, fill_version: bool = True):
version = package.get('Version')
if version:
version = version.split(':')
version = version[0] if len(version) == 1 else version[1]
pkg.id = package.get('ID')
pkg.name = package.get('Name')
if fill_version:
pkg.version = version
pkg.latest_version = version
pkg.description = package.get('Description')
pkg.package_base = package.get('PackageBase')
pkg.popularity = package.get('Popularity')
pkg.votes = package.get('NumVotes')
pkg.maintainer = package.get('Maintainer')
pkg.url_download = URL_PKG_DOWNLOAD.format(package['URLPath']) if package.get('URLPath') else None
pkg.first_submitted = datetime.fromtimestamp(package['FirstSubmitted']) if package.get('FirstSubmitted') else None
pkg.last_modified = datetime.fromtimestamp(package['LastModified']) if package.get('LastModified') else None
pkg.update = pkg.version and pkg.latest_version and pkg.latest_version > pkg.version
def fill_package_build(self, pkg: ArchPackage):
res = self.http_client.get(pkg.get_pkg_build_url())
if res and res.status_code == 200 and res.text:
pkg.pkgbuild = res.text
def map_api_data(self, apidata: dict, installed: dict) -> ArchPackage:
data = installed.get(apidata.get('Name'))
app = ArchPackage(name=apidata.get('Name'), installed=bool(data), mirror='aur')
if data:
app.version = data.get('version')
app.description = data.get('description')
self.fill_api_data(app, apidata, fill_version=not data)
return app

20
bauh/gems/arch/message.py Normal file
View File

@@ -0,0 +1,20 @@
from bauh.api.abstract.handler import ProcessWatcher
from bauh.api.abstract.view import MessageType
def show_dep_not_installed(watcher: ProcessWatcher, pkgname: str, depname: str, i18n: dict):
watcher.show_message(title=i18n['error'],
body=i18n['arch.install.dependency.install.error'].format('"{}"'.format(depname), '"{}"'.format(pkgname)),
type_=MessageType.ERROR)
def show_dep_not_found(depname: str, i18n: dict, watcher: ProcessWatcher):
watcher.show_message(title=i18n['arch.install.dep_not_found.title'],
body=i18n['arch.install.dep_not_found.body'].format('"{}"'.format(depname)),
type_=MessageType.ERROR)
def show_optdep_not_installed(depname: str, watcher: ProcessWatcher, i18n: dict):
watcher.show_message(title=i18n['error'],
body=i18n['arch.install.optdep.error'].format('"{}"'.format(depname)),
type_=MessageType.ERROR)

109
bauh/gems/arch/model.py Normal file
View File

@@ -0,0 +1,109 @@
import datetime
from bauh.api.abstract.model import SoftwarePackage
from bauh.api.constants import CACHE_PATH
from bauh.commons import resource
from bauh.gems.arch import ROOT_DIR
class ArchPackage(SoftwarePackage):
def __init__(self, name: str = None, version: str = None, latest_version: str = None, description: str = None,
package_base: str = None, votes: int = None, popularity: float = None,
first_submitted: datetime.datetime = None, last_modified: datetime.datetime = None,
maintainer: str = None, url_download: str = None, pkgbuild: str = None, mirror: str = None,
desktop_entry: str = None, installed: bool = False):
super(ArchPackage, self).__init__(name=name, version=version, latest_version=latest_version, description=description, installed=installed)
self.package_base = package_base
self.votes = votes
self.popularity = popularity
self.maintainer = maintainer
self.url_download = url_download
self.first_submitted = first_submitted
self.last_modified = last_modified
self.pkgbuild = pkgbuild
self.mirror = mirror
self.command = None
self.icon_path = None
self.downgrade_enabled = False
self.desktop_entry = desktop_entry
@staticmethod
def disk_cache_path(pkgname: str, mirror: str):
return CACHE_PATH + '/arch/installed/' + ('aur' if mirror == 'aur' else 'mirror') + '/' + pkgname
def get_pkg_build_url(self):
if self.package_base:
return 'https://aur.archlinux.org/cgit/aur.git/plain/PKGBUILD?h=' + self.package_base
def has_history(self):
return self.installed
def has_info(self):
return True
def can_be_installed(self):
return super(ArchPackage, self).can_be_installed() and self.url_download
def can_be_downgraded(self):
return self.installed and self.downgrade_enabled
def get_type(self):
return 'aur' if self.mirror == 'aur' else 'arch'
def get_default_icon_path(self) -> str:
return self.get_type_icon_path()
def get_disk_icon_path(self) -> str:
return self.icon_path
def get_type_icon_path(self):
return resource.get_path('img/arch.png', ROOT_DIR) # TODO change icon when from mirrors
def is_application(self):
return True
def supports_disk_cache(self):
return True
def get_disk_cache_path(self) -> str:
if self.name:
return self.disk_cache_path(self.name, self.mirror)
def get_data_to_cache(self) -> dict:
cache = {}
# required attrs to cache
for a in {'command', 'icon_path', 'mirror'}:
val = getattr(self, a)
if val:
cache[a] = val
else:
return None
if self.desktop_entry:
cache['desktop_entry'] = self.desktop_entry
return cache
def fill_cached_data(self, data: dict):
for a in {'command', 'icon_path', 'mirror', 'desktop_entry'}:
val = data.get(a)
if val:
setattr(self, a, val)
if a == 'icon_path':
self.icon_url = val
def can_be_run(self) -> bool:
# only returns if there is a desktop entry set for the application to avoid running command-line applications
return bool(self.desktop_entry)
def get_command(self) -> str:
return self.command
def get_publisher(self):
return self.maintainer

131
bauh/gems/arch/pacman.py Normal file
View File

@@ -0,0 +1,131 @@
import re
from typing import List, Set
from bauh.commons.system import run_cmd, new_subprocess, new_root_subprocess, SystemProcess
def is_enabled() -> bool:
try:
new_subprocess(['pacman', '--version'])
return True
except FileNotFoundError:
return False
def get_mirrors(pkgs: Set[str]) -> dict:
pkgre = '|'.join(pkgs)
searchres = new_subprocess(['pacman', '-Ss', pkgre]).stdout
mirrors = {}
for line in new_subprocess(['grep', '-E', '.+/({}) '.format(pkgre)], stdin=searchres).stdout:
if line:
match = line.decode()
for p in pkgs:
if p in match:
mirrors[p] = match.split('/')[0]
return mirrors
def is_available_from_mirrors(pkg_name: str) -> bool:
return bool(run_cmd('pacman -Ss ' + pkg_name))
def get_info(pkg_name) -> str:
return run_cmd('pacman -Qi ' + pkg_name)
def get_info_list(pkg_name: str) -> List[tuple]:
info = get_info(pkg_name)
if info:
return re.findall(r'(\w+\s?\w+)\s+:\s+(.+(\n\s+.+)*)', info)
def get_info_dict(pkg_name: str) -> dict:
info_list = get_info_list(pkg_name)
if info_list:
info_dict = {}
for info_data in info_list:
attr = info_data[0].lower()
info_dict[attr] = info_data[1] if '\n' not in info_data[1] else ' '.join([l.strip() for l in info_data[1].split('\n')])
if info_dict[attr] == 'None':
info_dict[attr] = None
return info_dict
def list_installed() -> Set[str]:
return {out.decode().strip() for out in new_subprocess(['pacman', '-Qq']).stdout if out}
def list_and_map_installed() -> dict: # returns a dict with with package names as keys and versions as values
installed = new_subprocess(['pacman', '-Qq']).stdout # retrieving all installed package names
allinfo = new_subprocess(['pacman', '-Qi'], stdin=installed).stdout # retrieving all installed packages info
pkgs, current_pkg = {'mirrors': {}, 'not_signed': {}}, {}
for out in new_subprocess(['grep', '-E', '(Name|Description|Version|Validated By)'], stdin=allinfo).stdout: # filtering only the Name and Validated By fields:
if out:
line = out.decode()
if line.startswith('Name'):
current_pkg['name'] = line.split(':')[1].strip()
elif line.startswith('Version'):
version = line.split(':')
current_pkg['version'] = version[len(version) - 1].strip()
elif line.startswith('Description'):
current_pkg['description'] = line.split(':')[1].strip()
elif line.startswith('Validated'):
if line.split(':')[1].strip().lower() == 'none':
pkgs['not_signed'][current_pkg['name']] = {'version': current_pkg['version'], 'description': current_pkg['description']}
current_pkg = {}
return pkgs
def install_as_process(pkgpath: str, root_password: str, aur: bool, pkgdir: str = '.') -> SystemProcess:
if aur:
cmd = ['pacman', '-U', pkgpath, '--noconfirm'] # pkgpath = install file path
else:
cmd = ['pacman', '-S', pkgpath, '--noconfirm'] # pkgpath = pkgname
return SystemProcess(new_root_subprocess(cmd, root_password, cwd=pkgdir))
def list_desktop_entries(pkgnames: Set[str]) -> List[str]:
if pkgnames:
installed_files = new_subprocess(['pacman', '-Qlq', *pkgnames]).stdout
desktop_files = []
for out in new_subprocess(['grep', '-E', '.desktop'], stdin=installed_files).stdout:
if out:
desktop_files.append(out.decode().strip())
return desktop_files
def list_icon_paths(pkgnames: Set[str]) -> List[str]:
installed_files = new_subprocess(['pacman', '-Qlq', *pkgnames]).stdout
icon_files = []
for out in new_subprocess(['grep', '-E', '.(png|svg|jpeg)'], stdin=installed_files).stdout:
if out:
icon_files.append(out.decode().strip())
return icon_files
def list_bin_paths(pkgnames: Set[str]) -> List[str]:
installed_files = new_subprocess(['pacman', '-Qlq', *pkgnames]).stdout
bin_paths = []
for out in new_subprocess(['grep', '-E', '^/usr/bin/.+'], stdin=installed_files).stdout:
if out:
bin_paths.append(out.decode().strip())
return bin_paths

View File

@@ -0,0 +1,13 @@
import re
from typing import Set
RE_PKGBUILD_OPTDEPS = re.compile(r"optdepends = ([^:\s]+)")
RE_PKGBUILD_DEPSON = re.compile(r"depends = ([^:\s]+)")
def read_optdeps(srcinfo: str) -> Set[str]:
return set(RE_PKGBUILD_OPTDEPS.findall(srcinfo))
def read_depends_on(srcinfo: str) -> Set[str]:
return set(RE_PKGBUILD_DEPSON.findall(srcinfo))

Binary file not shown.

After

Width:  |  Height:  |  Size: 905 B

View File

@@ -0,0 +1,49 @@
gem.arch.label=Arch ( AUR )
arch.install.conflict.popup.title=Conflict detected
arch.install.conflict.popup.body=The applications {} are in conflict. You must uninstall one to install the other. Continue ?
arch.missing_deps.title=Missing dependencies
arch.missing_deps.body=The following dependencies must be installed before the {} installation continues
arch.downgrade.error=Error
arch.downgrade.impossible=It is not possible to downgrade {}
aur.history.1_version=version
aur.history.2_release=release
aur.history.3_date=date
arch.downloading.package=Downloading the package
arch.uncompressing.package=Uncompressing the package
arch.checking.deps=Checking {} dependencies
arch.missing_deps_found=Missing dependencies for {}
arch.building.package=Building package {}
arch.checking.conflicts=Checking any conflicts with {}
arch.installing.package=Installing {} package
arch.uninstalling.conflict=Uninstalling conflicting package {}
arch.uninstalling.conflict.fail=It was not possible to uninstall the conflicting package {}
arch.clone=Cloning the AUR repository {}
arch.downgrade.reading_commits=Reading the repository commits
arch.downgrade.version_found=Current package version found
arch.downgrade.install_older=Installing older version
aur.info.01_id=id
aur.info.02_name=name
aur.info.03_version=version
aur.info.04_popularity=popularity
aur.info.05_votes=votes
aur.info.06_package_base=package base
aur.info.07_maintainer=maintainer
aur.info.08_first_submitted=first submitted
aur.info.09_last_modified=app.last_modified
aur.info.10_url=url download
aur.info.11_pkg_build_url=url pkgbuild
aur.info.13_pkg_build=pkgbuild
aur.info.11_dependson=dependencies
aur.info.12_optdepends=optional dependencies
arch.install.dep_not_found.title=Dependency not found
arch.install.dep_not_found.body=Required dependency {} was not found in AUR nor in default mirrors. Installation cancelled.
arch.install.dependency.install=Installing package dependency {}
arch.install.dependency.install.error=Could not install dependent package {}. Installation of {} aborted.
arch.uninstall.required_by={} cannot be uninstalled because it is necessary for these following packages to work
arch.uninstall.required_by.advice=Uninstall them first before uninstalling {}.
arch.install.optdeps.request.title=Optional dependencies
arch.install.optdeps.request.body={} was succesfully installed ! There are some optional associated packages that you might want to install as well (check those you want)
arch.install.optdep.error=Could not install the optional package {}
arch.optdeps.checking=Checking {} optional dependencies
arch.warning.disabled={} seems not to be installed. It will not be possible to manage Arch / AUR packages.
arch.warning.git={} seems not to be installed. It will not be possible to downgrade AUR packages.

View File

@@ -0,0 +1,81 @@
gem.arch.label=Arch ( AUR )
aur.info.architecture=arquitectura
aur.info.architecture.any=cualquier
aur.info.build date=fecha de construcción
aur.info.depends on=depende
aur.info.description=descripción
aur.info.install date=fecha de instalación
aur.info.install reason=razón da instalación
aur.info.install reason.explicitly installed=instalado explícitamente
aur.info.install script=script de instalación
aur.info.install script.no=ninguno
aur.info.installed size=tamaño da instalación
aur.info.licenses=licencias
aur.info.name=nombre
aur.info.optional deps=dependencias opcionales
aur.info.packager=empaquetador
aur.info.packager.unknown packager=desconocido
aur.info.url=url
aur.info.version=versión
aur.info.arch=arquitectura
aur.info.arch.any=cualquier
aur.info.depends=depende
aur.info.pkgdesc=descripción
aur.info.pkgname=nombre
aur.info.pkgrel=lanzamiento
aur.info.pkgver=versión
aur.info.source=origen
aur.info.optdepends=dependencias opcionales
aur.info.license=licencia
aur.info.validpgpkeys=llaves PGP válidas
aur.info.options=opciones
aur.info.provides=provee
aur.info.conflicts with=conflicta
arch.install.conflict.popup.title=Conflicto detectado
arch.install.conflict.popup.body=Los aplicativos {} estan en conflicto. Debe desinstalar uno para instalar el otro. ¿Continuar?
arch.missing_deps.title=Dependencias faltantes
arch.missing_deps.body=Deben instalarse las siguientes dependencias antes de continuar la instalación de {}
arch.downgrade.error=Error
arch.downgrade.impossible=No es posible revertir la versión de {}
aur.history.1_version=versión
aur.history.2_release=lanzamiento
aur.history.3_date=fecha
arch.downloading.package=Descargando el paquete
arch.uncompressing.package=Descomprimindo el paquete
arch.checking.deps=Verificando las dependencias de {}
arch.missing_deps_found=Dependencias faltantes para {}
arch.building.package=Construyendo el paquete {}
arch.checking.conflicts=Verificando se hay conflictos con {}
arch.installing.package=Instalando el paquete {}
arch.uninstalling.conflict=Eliminando el paquete conflictivo {}
arch.uninstalling.conflict.fail=No fue posible desinstalar el paquete conflictivo {}
arch.clone=Clonando el repositorio {} de AUR
arch.downgrade.reading_commits=Leyendo los commits del repositorio
arch.downgrade.version_found=Version actual del paquete encontrada
arch.downgrade.install_older=Instalando versión anterior
aur.info.01_id=id
aur.info.02_name=nombre
aur.info.03_version=versión
aur.info.04_popularity=popularidad
aur.info.05_votes=votos
aur.info.06_package_base=paquete base
aur.info.07_maintainer=mantenedor
aur.info.08_first_submitted=primero envio
aur.info.09_last_modified=última modificación
aur.info.10_url=url download
aur.info.11_pkg_build_url=url pkgbuild
aur.info.13_pkg_build=pkgbuild
aur.info.11_dependson=dependencias
aur.info.12_optdepends=dependencias opcionales
arch.install.dep_not_found.title=Dependencia no encontrada
arch.install.dep_not_found.body=No se encontró la dependencia requerida {} en AUR ni en los espejos predeterminados. Instalación cancelada.
arch.install.dependency.install=Instalando el paquete dependiente {}
arch.install.dependency.install.error=No se pudo instalar el paquete dependiente {}. Instalación de {} abortada.
arch.uninstall.required_by=No se puede desinstalar {} porque es necesario para que los siguientes paquetes funcionen
arch.uninstall.required_by.advice=Debe desinstalarlos primero antes de desinstalar {}
arch.install.optdeps.request.title=Dependencias opcionales
arch.install.optdeps.request.body=¡{} se instaló correctamente! También hay algunos paquetes opcionales asociados que es posible que desee instalar (marque los que desee)
arch.install.optdep.error=No se pudo instalar el paquete opcional {}
arch.optdeps.checking=Verificando las dependencias opcionales de {}
arch.warning.disabled={} parece no estar instalado. No será posible administrar paquetes Arch / AUR.
arch.warning.git={} parece no estar instalado. No será posible revertir las versiones de paquetes Arch / AUR.

View File

@@ -0,0 +1,81 @@
gem.arch.label=Arch ( AUR )
aur.info.architecture=arquitetura
aur.info.architecture.any=qualquer
aur.info.build date=data de construção
aur.info.depends on=depende
aur.info.description=descrição
aur.info.install date=data de instalação
aur.info.install reason=razão da instalação
aur.info.install reason.explicitly installed=instalado explicitamente
aur.info.install script=script de instalação
aur.info.install script.no=nenhum
aur.info.installed size=tamanho da instalação
aur.info.licenses=licenças
aur.info.name=nome
aur.info.optional deps=dependências opcionais
aur.info.packager=empacotador
aur.info.packager.unknown packager=desconhecido
aur.info.url=url
aur.info.version=versão
aur.info.arch=arquitetura
aur.info.arch.any=qualquer
aur.info.depends=depende
aur.info.pkgdesc=descrição
aur.info.pkgname=nome
aur.info.pkgrel=lançamento
aur.info.pkgver=versão
aur.info.source=origem
aur.info.optdepends=dependências opcionais
aur.info.license=licença
aur.info.validpgpkeys=chaves PGP válidas
aur.info.options=opções
aur.info.provides=provê
aur.info.conflicts with=conflita
arch.install.conflict.popup.title=Conflito detectado
arch.install.conflict.popup.body=Os aplicativos {} estão em conflito. Você precisa desinstalar um para instalar o outro. Continuar ?
arch.missing_deps.title=Dependências ausentes
arch.missing_deps.body=As seguintes dependências devem ser instaladas antes de continuar com a instalação de {}
arch.downgrade.error=Erro
arch.downgrade.impossible=Não é possível reverter a versão de {}
aur.history.1_version=versão
aur.history.2_release=lançamento
aur.history.3_date=data
arch.downloading.package=Baixando o pacote
arch.uncompressing.package=Descompactando o pacote
arch.checking.deps=Checando as dependências de {}
arch.missing_deps_found=Dependencias ausentes para {}
arch.building.package=Construindo o pacote {}
arch.checking.conflicts=Verificando se há conflitos com {}
arch.installing.package=Instalando o pacote {}
arch.uninstalling.conflict=Desinstalando o pacote conflitante {}
arch.uninstalling.conflict.fail=Não foi possível desinstalar o pacote conflitante {}
arch.clone=Clonando o repositório {} do AUR
arch.downgrade.reading_commits=Lendo os commits do repositório
arch.downgrade.version_found=Versão atual do pacote encontrada
arch.downgrade.install_older=Instalando versão anterior
aur.info.01_id=id
aur.info.02_name=nome
aur.info.03_version=versão
aur.info.04_popularity=popularidade
aur.info.05_votes=votos
aur.info.06_package_base=pacote base
aur.info.07_maintainer=mantenedor
aur.info.08_first_submitted=primeira submissão
aur.info.09_last_modified=última modificação
aur.info.10_url=url download
aur.info.11_pkg_build_url=url pkgbuild
aur.info.13_pkg_build=pkgbuild
aur.info.11_dependson=dependências
aur.info.12_optdepends=dependências opcionais
arch.install.dep_not_found.title=Dependência não encontrada
arch.install.dep_not_found.body=A dependência {} não foi encontrado no AUR nem nos espelhos padrões. Instalação cancelada.
arch.install.dependency.install=Instalando o pacote dependente {}
arch.install.dependency.install.error=Não foi possível instalar o pacote dependente {}. Instalação de {} abortada.
arch.uninstall.required_by={} não pode ser desinstalado porque ele é necessário para o funcionamento dos seguintes pacotes
arch.uninstall.required_by.advice=Desinstale eles primeiro antes de desinstalar {}
arch.install.optdeps.request.title=Dependências opcionais
arch.install.optdeps.request.body={} foi instalado com sucesso ! Existem alguns pacotes opcionais associados que talvez você também queira instalar (marque os desejados)
arch.install.optdep.error=Não foi possível instalar o pacote opcional {}
arch.optdeps.checking=Verificando as dependências opcionais de {}
arch.warning.disabled={} parece não estar instalado. Não será possível gerenciar pacotes Arch / AUR.
arch.warning.git={} parece não estar instalado. Não será possível reverter versões de pacotes Arch / AUR.

View File

@@ -0,0 +1,3 @@
from bauh.api.abstract.model import SuggestionPriority
ALL = {'google-chrome': SuggestionPriority.HIGH}

53
bauh/gems/arch/worker.py Normal file
View File

@@ -0,0 +1,53 @@
import logging
import os
import time
from multiprocessing import Process
from threading import Thread
from bauh.api.abstract.context import ApplicationContext
from bauh.api.abstract.controller import SoftwareManager
from bauh.gems.arch import pacman, disk
URL_INDEX = 'https://aur.archlinux.org/packages.gz'
URL_INFO = 'https://aur.archlinux.org/rpc/?v=5&type=info&arg={}'
class AURIndexUpdater(Thread):
def __init__(self, context: ApplicationContext, man: SoftwareManager):
super(AURIndexUpdater, self).__init__(daemon=True)
self.http_client = context.http_client
self.logger = context.logger
self.man = man
def run(self):
while True:
self.logger.info('Pre-indexing AUR packages in memory')
res = self.http_client.get(URL_INDEX)
if res and res.text:
self.man.names_index = {n.replace('-', '').replace('_', '').replace('.', ''): n for n in res.text.split('\n') if n and not n.startswith('#')}
self.logger.info('Pre-indexed {} AUR package names in memory'.format(len(self.man.names_index)))
else:
self.logger.warning('No data returned from: {}'.format(URL_INDEX))
time.sleep(5 * 60) # updates every 5 minutes
class ArchDiskCacheUpdater(Thread if bool(os.getenv('BAUH_DEBUG', 0)) else Process):
def __init__(self, logger: logging.Logger):
super(ArchDiskCacheUpdater, self).__init__(daemon=True)
self.logger = logger
def run(self):
self.logger.info('Pre-caching installed AUR packages data to disk')
installed = pacman.list_and_map_installed()
saved = 0
if installed and installed['not_signed']:
saved = disk.save_several({app for app in installed['not_signed']}, 'aur', overwrite=False)
self.logger.info('Pre-cached data of {} AUR packages to disk'.format(saved))

View File

@@ -0,0 +1,3 @@
import os
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))

View File

@@ -0,0 +1,2 @@
FLATHUB_URL = 'https://flathub.org'
FLATHUB_API_URL = FLATHUB_URL + '/api/v1'

View File

@@ -0,0 +1,208 @@
from datetime import datetime
from typing import List, Set, Type
from bauh.api.abstract.controller import SearchResult, SoftwareManager, ApplicationContext
from bauh.api.abstract.disk import DiskCacheLoader
from bauh.api.abstract.handler import ProcessWatcher
from bauh.api.abstract.model import PackageHistory, PackageUpdate, SoftwarePackage, PackageSuggestion
from bauh.api.abstract.view import MessageType
from bauh.commons.html import strip_html
from bauh.commons.system import SystemProcess, ProcessHandler
from bauh.gems.flatpak import flatpak, suggestions
from bauh.gems.flatpak.model import FlatpakApplication
from bauh.gems.flatpak.worker import FlatpakAsyncDataLoader, FlatpakUpdateLoader
class FlatpakManager(SoftwareManager):
def __init__(self, context: ApplicationContext):
super(FlatpakManager, self).__init__(context=context)
self.i18n = context.i18n
self.api_cache = context.cache_factory.new()
context.disk_loader_factory.map(FlatpakApplication, self.api_cache)
def get_managed_types(self) -> Set["type"]:
return {FlatpakApplication}
def _map_to_model(self, app_json: dict, installed: bool, disk_loader: DiskCacheLoader) -> FlatpakApplication:
app = FlatpakApplication(arch=app_json.get('arch'),
branch=app_json.get('branch'),
origin=app_json.get('origin'),
runtime=app_json.get('runtime'),
ref=app_json.get('ref'),
commit=app_json.get('commit'),
id=app_json.get('id'),
name=app_json.get('name'),
version=app_json.get('version'),
latest_version=app_json.get('latest_version'))
app.installed = installed
api_data = self.api_cache.get(app_json['id'])
expired_data = api_data and api_data.get('expires_at') and api_data['expires_at'] <= datetime.utcnow()
if not api_data or expired_data:
if not app_json['runtime']:
if disk_loader:
disk_loader.fill(app) # preloading cached disk data
FlatpakAsyncDataLoader(app=app, api_cache=self.api_cache, manager=self, context=self.context).start()
else:
app.fill_cached_data(api_data)
return app
def search(self, words: str, disk_loader: DiskCacheLoader, limit: int = -1) -> SearchResult:
res = SearchResult([], [], 0)
apps_found = flatpak.search(words)
if apps_found:
already_read = set()
installed_apps = self.read_installed(disk_loader=disk_loader).installed
if installed_apps:
for app_found in apps_found:
for installed_app in installed_apps:
if app_found['id'] == installed_app.id:
res.installed.append(installed_app)
already_read.add(app_found['id'])
if len(apps_found) > len(already_read):
for app_found in apps_found:
if app_found['id'] not in already_read:
res.new.append(self._map_to_model(app_found, False, disk_loader))
res.total = len(res.installed) + len(res.new)
return res
def read_installed(self, disk_loader: DiskCacheLoader, limit: int = -1, only_apps: bool = False, pkg_types: Set[Type[SoftwarePackage]] = None) -> SearchResult:
installed = flatpak.list_installed()
models = []
if installed:
available_updates = flatpak.list_updates_as_str()
for app_json in installed:
model = self._map_to_model(app_json=app_json, installed=True, disk_loader=disk_loader)
model.update = app_json['id'] in available_updates
models.append(model)
return SearchResult(models, None, len(models))
def downgrade(self, pkg: FlatpakApplication, root_password: str, watcher: ProcessWatcher) -> bool:
watcher.change_progress(10)
watcher.change_substatus(self.i18n['flatpak.downgrade.commits'])
commits = flatpak.get_app_commits(pkg.ref, pkg.origin)
commit_idx = commits.index(pkg.commit)
# downgrade is not possible if the app current commit in the first one:
if commit_idx == len(commits) - 1:
watcher.show_message(self.i18n['flatpak.downgrade.impossible.title'], self.i18n['flatpak.downgrade.impossible.body'], MessageType.WARNING)
return False
commit = commits[commit_idx + 1]
watcher.change_substatus(self.i18n['flatpak.downgrade.reverting'])
watcher.change_progress(50)
success = ProcessHandler(watcher).handle(SystemProcess(subproc=flatpak.downgrade(pkg.ref, commit, root_password), success_phrase='Updates complete.'))
watcher.change_progress(100)
return success
def clean_cache_for(self, pkg: FlatpakApplication):
super(FlatpakManager, self).clean_cache_for(pkg)
self.api_cache.delete(pkg.id)
def update(self, pkg: FlatpakApplication, root_password: str, watcher: ProcessWatcher) -> bool:
return ProcessHandler(watcher).handle(SystemProcess(subproc=flatpak.update(pkg.ref)))
def uninstall(self, pkg: FlatpakApplication, root_password: str, watcher: ProcessWatcher) -> bool:
return ProcessHandler(watcher).handle(SystemProcess(subproc=flatpak.uninstall(pkg.ref)))
def get_info(self, app: FlatpakApplication) -> dict:
app_info = flatpak.get_app_info_fields(app.id, app.branch)
app_info['name'] = app.name
app_info['type'] = 'runtime' if app.runtime else 'app'
app_info['description'] = strip_html(app.description)
return app_info
def get_history(self, pkg: FlatpakApplication) -> PackageHistory:
commits = flatpak.get_app_commits_data(pkg.ref, pkg.origin)
status_idx = 0
for idx, data in enumerate(commits):
if data['commit'] == pkg.commit:
status_idx = idx
break
return PackageHistory(pkg=pkg, history=commits, pkg_status_idx=status_idx)
def install(self, pkg: FlatpakApplication, root_password: str, watcher: ProcessWatcher) -> bool:
return ProcessHandler(watcher).handle(SystemProcess(subproc=flatpak.install(pkg.id, pkg.origin)))
def is_enabled(self):
return flatpak.is_installed()
def requires_root(self, action: str, pkg: FlatpakApplication):
return action == 'downgrade'
def prepare(self):
pass
def list_updates(self) -> List[PackageUpdate]:
updates = []
installed = flatpak.list_installed(extra_fields=False)
if installed:
available_updates = flatpak.list_updates_as_str()
if available_updates:
loaders = None
for app_json in installed:
if app_json['id'] in available_updates:
loader = FlatpakUpdateLoader(app=app_json, http_client=self.context.http_client)
loader.start()
if loaders is None:
loaders = []
loaders.append(loader)
if loaders:
for loader in loaders:
loader.join()
app = loader.app
updates.append(PackageUpdate(pkg_id='{}:{}'.format(app['id'], app['branch']),
pkg_type='flatpak',
version=app.get('version')))
return updates
def list_warnings(self) -> List[str]:
if flatpak.is_installed():
if not flatpak.has_remotes_set():
return [self.i18n['flatpak.notification.no_remotes']]
def list_suggestions(self, limit: int) -> List[PackageSuggestion]:
res = []
sugs = [(i, p) for i, p in suggestions.ALL.items()]
sugs.sort(key=lambda t: t[1].value, reverse=True)
for sug in sugs:
if limit <= 0 or len(res) < limit:
app_json = flatpak.search(sug[0], app_id=True)
if app_json:
res.append(PackageSuggestion(self._map_to_model(app_json[0], False, None), sug[1]))
else:
break
return res

245
bauh/gems/flatpak/flatpak.py Executable file
View File

@@ -0,0 +1,245 @@
import re
import subprocess
from typing import List
from bauh.api.exception import NoInternetException
from bauh.commons.system import new_subprocess, run_cmd, new_root_subprocess
BASE_CMD = 'flatpak'
def app_str_to_json(line: str, version: str, extra_fields: bool = True) -> dict:
app_array = line.split('\t')
if version >= '1.3.0':
app = {'name': app_array[0],
'id': app_array[1],
'version': app_array[2],
'branch': app_array[3]}
elif '1.0' <= version < '1.1':
ref_data = app_array[0].split('/')
app = {'id': ref_data[0],
'arch': ref_data[1],
'name': ref_data[0].split('.')[-1],
'ref': app_array[0],
'options': app_array[1],
'branch': ref_data[2],
'version': None}
elif '1.2' <= version < '1.3':
app = {'id': app_array[1],
'name': app_array[1].strip().split('.')[-1],
'version': app_array[2],
'branch': app_array[3],
'arch': app_array[4],
'origin': app_array[5]}
else:
raise Exception('Unsupported version')
if extra_fields:
extra_fields = get_app_info_fields(app['id'], app['branch'], ['origin', 'arch', 'ref', 'commit'], check_runtime=True)
app.update(extra_fields)
return app
def get_app_info_fields(app_id: str, branch: str, fields: List[str] = [], check_runtime: bool = False):
info = re.findall(r'\w+:\s.+', get_app_info(app_id, branch))
data = {}
fields_to_retrieve = len(fields) + (1 if check_runtime and 'ref' not in fields else 0)
for field in info:
if fields and fields_to_retrieve == 0:
break
field_val = field.split(':')
field_name = field_val[0].lower()
if not fields or field_name in fields or (check_runtime and field_name == 'ref'):
data[field_name] = field_val[1].strip()
if fields:
fields_to_retrieve -= 1
if check_runtime and field_name == 'ref':
data['runtime'] = data['ref'].startswith('runtime/')
return data
def is_installed():
version = get_version()
return False if version is None else True
def get_version():
res = run_cmd('{} --version'.format(BASE_CMD), print_error=False)
return res.split(' ')[1].strip() if res else None
def get_app_info(app_id: str, branch: str):
return run_cmd('{} info {} {}'.format(BASE_CMD, app_id, branch))
def list_installed(extra_fields: bool = True) -> List[dict]:
apps_str = run_cmd('{} list'.format(BASE_CMD))
if apps_str:
version = get_version()
app_lines = apps_str.split('\n')
return [app_str_to_json(line, version, extra_fields=extra_fields) for line in app_lines if line]
return []
def update(app_ref: str):
"""
Updates the app reference
:param app_ref:
:return:
"""
return new_subprocess([BASE_CMD, 'update', '-y', app_ref])
def uninstall(app_ref: str):
"""
Removes the app by its reference
:param app_ref:
:return:
"""
return new_subprocess([BASE_CMD, 'uninstall', app_ref, '-y'])
def list_updates_as_str():
return run_cmd('{} update'.format(BASE_CMD), ignore_return_code=True)
def downgrade(app_ref: str, commit: str, root_password: str) -> subprocess.Popen:
return new_root_subprocess([BASE_CMD, 'update', '--commit={}'.format(commit), app_ref, '-y'], root_password)
def get_app_commits(app_ref: str, origin: str) -> List[str]:
log = run_cmd('{} remote-info --log {} {}'.format(BASE_CMD, origin, app_ref))
if log:
return re.findall(r'Commit+:\s(.+)', log)
else:
raise NoInternetException()
def get_app_commits_data(app_ref: str, origin: str) -> List[dict]:
log = run_cmd('{} remote-info --log {} {}'.format(BASE_CMD, origin, app_ref))
if not log:
raise NoInternetException()
res = re.findall(r'(Commit|Subject|Date):\s(.+)', log)
commits = []
commit = {}
for idx, data in enumerate(res):
commit[data[0].strip().lower()] = data[1].strip()
if (idx + 1) % 3 == 0:
commits.append(commit)
commit = {}
return commits
def search(word: str, app_id: bool = False) -> List[dict]:
cli_version = get_version()
res = run_cmd('{} search {}'.format(BASE_CMD, word))
found = []
split_res = res.split('\n')
if split_res and split_res[0].lower() != 'no matches found':
for info in split_res:
if info:
info_list = info.split('\t')
if cli_version >= '1.3.0':
id_ = info_list[2].strip()
if app_id and id_ != word:
continue
version = info_list[3].strip()
app = {
'name': info_list[0].strip(),
'description': info_list[1].strip(),
'id': id_,
'version': version,
'latest_version': version,
'branch': info_list[4].strip(),
'origin': info_list[5].strip(),
'runtime': False,
'arch': None, # unknown at this moment,
'ref': None # unknown at this moment
}
elif cli_version >= '1.2.0':
id_ = info_list[1].strip()
if app_id and id_ != word:
continue
desc = info_list[0].split('-')
version = info_list[2].strip()
app = {
'name': desc[0].strip(),
'description': desc[1].strip(),
'id': id_,
'version': version,
'latest_version': version,
'branch': info_list[3].strip(),
'origin': info_list[4].strip(),
'runtime': False,
'arch': None, # unknown at this moment,
'ref': None # unknown at this moment
}
else:
id_ = info_list[0].strip()
if app_id and id_ != word:
continue
version = info_list[1].strip()
app = {
'name': '',
'description': info_list[4].strip(),
'id': id_,
'version': version,
'latest_version': version,
'branch': info_list[2].strip(),
'origin': info_list[3].strip(),
'runtime': False,
'arch': None, # unknown at this moment,
'ref': None # unknown at this moment
}
found.append(app)
if app_id and len(found) > 0:
break
return found
def install(app_id: str, origin: str):
return new_subprocess([BASE_CMD, 'install', origin, app_id, '-y'])
def set_default_remotes():
run_cmd('{} remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo'.format(BASE_CMD))
def has_remotes_set() -> bool:
return bool(run_cmd('{} remotes'.format(BASE_CMD)).strip())

View File

@@ -0,0 +1,69 @@
from bauh.api.abstract.model import SoftwarePackage
from bauh.commons import resource
from bauh.gems.flatpak import ROOT_DIR
class FlatpakApplication(SoftwarePackage):
def __init__(self, id: str = None, name: str = None, version: str = None, latest_version: str = None, description: str = None,
branch: str = None, arch: str = None, origin: str = None, runtime: bool = False, ref: str = str, commit: str = str):
super(FlatpakApplication, self).__init__(id=id, name=name, version=version,
latest_version=latest_version, description=description)
self.ref = ref
self.branch = branch
self.arch = arch
self.origin = origin
self.runtime = runtime
self.commit = commit
def is_incomplete(self):
return self.description is None and self.icon_url
def has_history(self):
return self.installed
def has_info(self):
return self.installed
def can_be_downgraded(self):
return self.installed
def get_type(self):
return 'flatpak'
def get_default_icon_path(self):
return resource.get_path('img/flathub.svg', ROOT_DIR)
def get_type_icon_path(self):
return self.get_default_icon_path()
def is_application(self):
return not self.runtime
def get_disk_cache_path(self):
return super(FlatpakApplication, self).get_disk_cache_path() + '/installed/' + self.id
def get_data_to_cache(self):
return {
'description': self.description,
'icon_url': self.icon_url,
'latest_version': self.latest_version,
'version': self.version,
'name': self.name,
'categories': self.categories
}
def fill_cached_data(self, data: dict):
for attr in self.get_data_to_cache().keys():
if data.get(attr) and not getattr(self, attr):
setattr(self, attr, data[attr])
def get_command(self) -> str:
return "flatpak run {}".format(self.id)
def can_be_run(self) -> bool:
return self.installed and not self.runtime
def get_publisher(self):
return 'flathub'

View File

@@ -0,0 +1,179 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="47.919052"
height="48.110374"
viewBox="0 0 12.678582 12.729203"
version="1.1"
id="svg3871"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
sodipodi:docname="flathub.svg">
<defs
id="defs3865" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.35"
inkscape:cx="8.4698372"
inkscape:cy="118.04149"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
showborder="false"
inkscape:window-width="1366"
inkscape:window-height="703"
inkscape:window-x="0"
inkscape:window-y="260"
inkscape:window-maximized="1"
fit-margin-bottom="-0.2"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
units="px" />
<metadata
id="metadata3868">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-12.122116,-167.33593)">
<g
transform="matrix(0.08875425,0,0,0.11490723,-57.513376,72.576168)"
id="g10674"
style="stroke-width:0.77710575">
<path
sodipodi:nodetypes="ccccccccccc"
inkscape:connector-curvature="0"
id="path10642"
d="m 856.01397,830.22631 -66.76365,43.44891 v 34.93973 l 33.38182,21.72315 33.37926,-21.72315 v -0.005 l 0.003,0.003 33.38183,21.72574 33.37925,-21.72315 v -34.93979 z"
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#204a87;fill-opacity:1;fill-rule:nonzero;stroke:#204a87;stroke-width:9.3252697;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<g
style="stroke-width:0.58728963"
transform="matrix(0.50221552,0,0,0.50636711,429.63205,432.90461)"
id="g10652">
<rect
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#204a87;fill-opacity:1;fill-rule:nonzero;stroke:#204a87;stroke-width:2.4608953;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
id="rect10644"
width="79.110413"
height="79.110413"
x="1289.9076"
y="279.42584"
rx="0"
ry="0"
transform="matrix(0.84019328,0.54228706,-0.84019328,0.54228706,0,0)" />
<path
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#204a87;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:2.34915853;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 915.46811,824.92983 -5e-5,68.99997 -66.46803,42.90055 5e-5,-68.99997 z"
id="path10648"
inkscape:connector-curvature="0" />
<path
inkscape:connector-curvature="0"
id="path10650"
d="m 783.00003,824.92983 5e-5,68.99997 66.46803,42.90055 -5e-5,-68.99997 z"
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:2.34915853;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<rect
transform="matrix(0.84019328,0.54228706,-0.84019328,0.54228706,0,0)"
ry="0"
rx="0"
y="215.8064"
x="1226.2882"
height="79.110413"
width="79.110413"
id="rect10646"
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#204a87;stroke-width:2.4608953;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
</g>
<g
style="stroke-width:0.58728963"
transform="matrix(0.50221552,0,0,0.50636711,463.01368,454.62849)"
id="g10662">
<rect
transform="matrix(0.84019328,0.54228706,-0.84019328,0.54228706,0,0)"
ry="0"
rx="0"
y="279.42584"
x="1289.9076"
height="79.110413"
width="79.110413"
id="rect10654"
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#204a87;fill-opacity:1;fill-rule:nonzero;stroke:#204a87;stroke-width:2.4608953;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<rect
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#204a87;stroke-width:2.4608953;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
id="rect10656"
width="79.110413"
height="79.110413"
x="1226.2882"
y="215.8064"
rx="0"
ry="0"
transform="matrix(0.84019328,0.54228706,-0.84019328,0.54228706,0,0)" />
<path
inkscape:connector-curvature="0"
id="path10658"
d="m 915.46811,824.92983 -5e-5,68.99997 -66.46803,42.90055 5e-5,-68.99997 z"
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#204a87;fill-opacity:1;fill-rule:nonzero;stroke:#204a87;stroke-width:2.34915853;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#204a87;stroke-width:2.34915853;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 783.00003,826.54445 5e-5,68.99997 66.46803,42.90055 -5e-5,-68.99997 z"
id="path10660"
inkscape:connector-curvature="0" />
</g>
<g
style="stroke-width:0.58728963"
id="g10672"
transform="matrix(0.50221552,0,0,0.50636711,396.25042,454.62849)">
<rect
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#204a87;fill-opacity:1;fill-rule:nonzero;stroke:#204a87;stroke-width:2.4608953;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
id="rect10664"
width="79.110413"
height="79.110413"
x="1289.9076"
y="279.42584"
rx="0"
ry="0"
transform="matrix(0.84019328,0.54228706,-0.84019328,0.54228706,0,0)" />
<rect
transform="matrix(0.84019328,0.54228706,-0.84019328,0.54228706,0,0)"
ry="0"
rx="0"
y="215.8064"
x="1226.2882"
height="79.110413"
width="79.110413"
id="rect10666"
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#204a87;stroke-width:2.4608953;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#204a87;fill-opacity:1;fill-rule:nonzero;stroke:#204a87;stroke-width:2.34915853;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 915.46811,824.92983 -5e-5,68.99997 -66.46803,42.90055 5e-5,-68.99997 z"
id="path10668"
inkscape:connector-curvature="0" />
<path
inkscape:connector-curvature="0"
id="path10670"
d="m 783.00003,824.92983 5e-5,68.99997 66.46803,42.90055 -5e-5,-68.99997 z"
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#204a87;stroke-width:2.34915853;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@@ -0,0 +1,5 @@
flatpak.notification.no_remotes=No Flatpak remotes set. It will not be possible to search for Flatpak apps.
flatpak.downgrade.impossible.title=Error
flatpak.downgrade.impossible.body=Impossible to downgrade: the app is in its first version
flatpak.downgrade.commits=Reading package commits
flatpak.downgrade.reverting=Reverting version

View File

@@ -0,0 +1,27 @@
flatpak.info.arch=arquitectura
flatpak.info.branch=rama
flatpak.info.collection=colección
flatpak.info.commit=commit
flatpak.info.date=fecha
flatpak.info.description=descripción
flatpak.info.id=id
flatpak.info.installation=instalación
flatpak.info.installed=instalado
flatpak.info.license=licencia
flatpak.info.name=nombre
flatpak.info.origin=origen
flatpak.info.parent=padre
flatpak.info.ref=ref
flatpak.info.runtime=runtime
flatpak.info.sdk=sdk
flatpak.info.subject=tema
flatpak.info.type=tipo
flatpak.info.version=versión
flatpak.notification.no_remotes=No hay repositorios (remotes) Flatpak configurados. No será posible buscar aplicativos Flatpak.
flatpak.downgrade.impossible.title=Error
flatpak.downgrade.impossible.body=Imposible revertir la versión: el aplicativo está en su primera versión
flatpak.history.date=fecha
flatpak.history.commit=commit
flatpak.history.subject=tema
flatpak.downgrade.commits=Leyendo commits del paquete
flatpak.downgrade.reverting=Revirtiendo la versión

View File

@@ -0,0 +1,27 @@
flatpak.info.arch=arquitetura
flatpak.info.branch=ramo
flatpak.info.collection=coleção
flatpak.info.commit=commit
flatpak.info.date=data
flatpak.info.description=descrição
flatpak.info.id=id
flatpak.info.installation=instalação
flatpak.info.installed=instalado
flatpak.info.license=licença
flatpak.info.name=nome
flatpak.info.origin=origem
flatpak.info.parent=pai
flatpak.info.ref=ref
flatpak.info.runtime=runtime
flatpak.info.sdk=sdk
flatpak.info.subject=assunto
flatpak.info.type=tipo
flatpak.info.version=versão
flatpak.notification.no_remotes=Não há repositórios (remotes) Flatpak configurados. Não será possível buscar aplicativos Flatpak.
flatpak.downgrade.impossible.title=Erro
flatpak.downgrade.impossible.body=Impossível reverter a versão: o aplicativo está na sua primeira versão
flatpak.history.date=data
flatpak.history.commit=commit
flatpak.history.subject=assunto
flatpak.downgrade.commits=Lendo os commits do pacote
flatpak.downgrade.reverting=Revertendo a versão

View File

@@ -0,0 +1,15 @@
from bauh.api.abstract.model import SuggestionPriority
ALL = {
'com.spotify.Client': SuggestionPriority.TOP,
'com.skype.Client': SuggestionPriority.HIGH,
'com.dropbox.Client': SuggestionPriority.MEDIUM,
'us.zoom.Zoom': SuggestionPriority.MEDIUM,
'org.telegram.desktop': SuggestionPriority.MEDIUM,
'com.visualstudio.code': SuggestionPriority.LOW,
'org.inkscape.Inkscape': SuggestionPriority.LOW,
'org.libretro.RetroArch': SuggestionPriority.LOW,
'org.kde.kdenlive': SuggestionPriority.LOW,
'org.videolan.VLC': SuggestionPriority.LOW
}

View File

@@ -0,0 +1,96 @@
import traceback
from threading import Thread
from bauh.api.abstract.cache import MemoryCache
from bauh.api.abstract.context import ApplicationContext
from bauh.api.abstract.controller import SoftwareManager
from bauh.api.abstract.model import PackageStatus
from bauh.api.http import HttpClient
from bauh.gems.flatpak import flatpak
from bauh.gems.flatpak.constants import FLATHUB_API_URL, FLATHUB_URL
from bauh.gems.flatpak.model import FlatpakApplication
class FlatpakAsyncDataLoader(Thread):
def __init__(self, app: FlatpakApplication, manager: SoftwareManager, context: ApplicationContext, api_cache: MemoryCache):
super(FlatpakAsyncDataLoader, self).__init__(daemon=True)
self.app = app
self.manager = manager
self.http_client = context.http_client
self.api_cache = api_cache
self.persist = False
self.logger = context.logger
def run(self):
if self.app:
self.app.status = PackageStatus.LOADING_DATA
try:
res = self.http_client.get('{}/apps/{}'.format(FLATHUB_API_URL, self.app.id))
if res.status_code == 200 and res.text:
data = res.json()
if not self.app.version:
self.app.version = data.get('version')
if not self.app.name:
self.app.name = data.get('name')
self.app.description = data.get('description', data.get('summary', None))
self.app.icon_url = data.get('iconMobileUrl', None)
self.app.latest_version = data.get('currentReleaseVersion', self.app.version)
if not self.app.version and self.app.latest_version:
self.app.version = self.app.latest_version
if not self.app.installed and self.app.latest_version:
self.app.version = self.app.latest_version
if self.app.icon_url and self.app.icon_url.startswith('/'):
self.app.icon_url = FLATHUB_URL + self.app.icon_url
if data.get('categories'):
self.app.categories = [c['name'] for c in data['categories']]
loaded_data = self.app.get_data_to_cache()
self.api_cache.add(self.app.id, loaded_data)
self.persist = self.app.supports_disk_cache()
else:
self.logger.warning("Could not retrieve app data for id '{}'. Server response: {}. Body: {}".format(self.app.id, res.status_code, res.content.decode()))
except:
self.logger.error("Could not retrieve app data for id '{}'".format(self.app.id))
traceback.print_exc()
self.app.status = PackageStatus.READY
if self.persist:
self.manager.cache_to_disk(pkg=self.app, icon_bytes=None, only_icon=False)
class FlatpakUpdateLoader(Thread):
def __init__(self, app: dict, http_client: HttpClient):
super(FlatpakUpdateLoader, self).__init__(daemon=True)
self.app = app
self.http_client = http_client
def run(self):
if self.app.get('ref') is None:
self.app.update(flatpak.get_app_info_fields(self.app['id'], self.app['branch'], fields=['ref'], check_runtime=True))
else:
self.app['runtime'] = self.app['ref'].startswith('runtime/')
if not self.app['runtime']:
try:
data = self.http_client.get_json('{}/apps/{}'.format(FLATHUB_API_URL, self.app['id']))
if data and data.get('currentReleaseVersion'):
self.app['version'] = data['currentReleaseVersion']
except:
traceback.print_exc()

View File

@@ -0,0 +1,2 @@
import os
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))

View File

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

View File

@@ -0,0 +1,145 @@
from datetime import datetime
from typing import List, Set, Type
from bauh.api.abstract.controller import SoftwareManager, SearchResult, ApplicationContext
from bauh.api.abstract.disk import DiskCacheLoader
from bauh.api.abstract.handler import ProcessWatcher
from bauh.api.abstract.model import SoftwarePackage, PackageHistory, PackageUpdate, PackageSuggestion
from bauh.commons.system import SystemProcess, ProcessHandler
from bauh.gems.snap import snap, suggestions
from bauh.gems.snap.model import SnapApplication
from bauh.gems.snap.worker import SnapAsyncDataLoader
class SnapManager(SoftwareManager):
def __init__(self, context: ApplicationContext):
super(SnapManager, self).__init__(context=context)
self.i18n = context.i18n
self.api_cache = context.cache_factory.new()
context.disk_loader_factory.map(SnapApplication, self.api_cache)
def map_json(self, app_json: dict, installed: bool, disk_loader: DiskCacheLoader) -> SnapApplication:
app = SnapApplication(publisher=app_json.get('publisher'),
rev=app_json.get('rev'),
notes=app_json.get('notes'),
app_type=app_json.get('type'),
id=app_json.get('name'),
name=app_json.get('name'),
version=app_json.get('version'),
latest_version=app_json.get('version'),
description=app_json.get('description'))
if app.publisher:
app.publisher = app.publisher.replace('*', '')
app.installed = installed
api_data = self.api_cache.get(app_json['name'])
expired_data = api_data and api_data.get('expires_at') and api_data['expires_at'] <= datetime.utcnow()
if (not api_data or expired_data) and app.is_application():
if disk_loader and app.installed:
disk_loader.fill(app)
SnapAsyncDataLoader(app=app, api_cache=self.api_cache, manager=self, context=self.context).start()
else:
app.fill_cached_data(api_data)
return app
def search(self, words: str, disk_loader: DiskCacheLoader, limit: int = -1) -> SearchResult:
installed = self.read_installed(disk_loader).installed
res = SearchResult([], [], 0)
for app_json in snap.search(words):
already_installed = None
if installed:
already_installed = [i for i in installed if i.id == app_json.get('name')]
already_installed = already_installed[0] if already_installed else None
if already_installed:
res.installed.append(already_installed)
else:
res.new.append(self.map_json(app_json, installed=False, disk_loader=disk_loader))
res.total = len(res.installed) + len(res.new)
return res
def read_installed(self, disk_loader: DiskCacheLoader, limit: int = -1, only_apps: bool = False, pkg_types: Set[Type[SoftwarePackage]] = None) -> SearchResult:
installed = [self.map_json(app_json, installed=True, disk_loader=disk_loader) for app_json in snap.read_installed()]
return SearchResult(installed, None, len(installed))
def downgrade(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher) -> bool:
return ProcessHandler(watcher).handle(SystemProcess(subproc=snap.downgrade_and_stream(pkg.name, root_password), wrong_error_phrase=None))
def update(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher) -> SystemProcess:
raise Exception("'update' is not supported by {}".format(pkg.__class__.__name__))
def uninstall(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher) -> bool:
return ProcessHandler(watcher).handle(SystemProcess(subproc=snap.uninstall_and_stream(pkg.name, root_password)))
def get_managed_types(self) -> Set[Type[SoftwarePackage]]:
return {SnapApplication}
def clean_cache_for(self, pkg: SnapApplication):
super(SnapManager, self).clean_cache_for(pkg)
self.api_cache.delete(pkg.id)
def get_info(self, pkg: SnapApplication) -> dict:
info = snap.get_info(pkg.name, attrs=('license', 'contact', 'commands', 'snap-id', 'tracking', 'installed'))
info['description'] = pkg.description
info['publisher'] = pkg.publisher
info['revision'] = pkg.rev
info['name'] = pkg.name
if info.get('commands'):
info['commands'] = ' '.join(info['commands'])
return info
def get_history(self, pkg: SnapApplication) -> PackageHistory:
raise Exception("'get_history' is not supported by {}".format(pkg.__class__.__name__))
def install(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher) -> bool:
return ProcessHandler(watcher).handle(SystemProcess(subproc=snap.install_and_stream(pkg.name, pkg.confinement, root_password)))
def is_enabled(self) -> bool:
return snap.is_installed()
def requires_root(self, action: str, pkg: SnapApplication):
return action != 'search'
def refresh(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher) -> bool:
return ProcessHandler(watcher).handle(SystemProcess(subproc=snap.refresh_and_stream(pkg.name, root_password)))
def prepare(self):
pass
def list_updates(self) -> List[PackageUpdate]:
pass
def list_warnings(self) -> List[str]:
if snap.get_snapd_version() == 'unavailable':
return [self.i18n['snap.notification.snapd_unavailable']]
def list_suggestions(self, limit: int) -> List[PackageSuggestion]:
res = []
sugs = [(i, p) for i, p in suggestions.ALL.items()]
sugs.sort(key=lambda t: t[1].value, reverse=True)
for sug in sugs:
if limit <= 0 or len(res) < limit:
found = snap.search(sug[0], exact_name=True)
if found:
res.append(PackageSuggestion(self.map_json(found[0], installed=False, disk_loader=None), sug[1]))
else:
break
return res

88
bauh/gems/snap/model.py Normal file
View File

@@ -0,0 +1,88 @@
from typing import List
from bauh.api.abstract.model import SoftwarePackage, PackageAction
from bauh.commons import resource
from bauh.gems.snap import ROOT_DIR
EXTRA_INSTALLED_ACTIONS = [
PackageAction(i18n_status_key='snap.action.refresh.status',
i18_label_key='snap.action.refresh.label',
icon_path=resource.get_path('img/refresh.svg', ROOT_DIR),
manager_method='refresh',
requires_root=True)
]
class SnapApplication(SoftwarePackage):
def __init__(self, id: str = None, name: str = None, version: str = None, latest_version: str = None,
description: str = None, publisher: str = None, rev: str = None, notes: str = None,
app_type: str = None, confinement: str = None):
super(SnapApplication, self).__init__(id=id, name=name, version=version,
latest_version=latest_version, description=description)
self.publisher = publisher
self.rev = rev
self.notes = notes
self.type = app_type
self.confinement = confinement
def has_history(self):
return False
def has_info(self):
return True
def can_be_downgraded(self):
return self.installed
def get_type(self):
return 'snap'
def get_default_icon_path(self):
return resource.get_path('img/snap.png', ROOT_DIR)
def get_type_icon_path(self):
return self.get_default_icon_path()
def is_application(self):
return not self.type and (self.type not in ('core', 'base', 'snapd') and not self._name_starts_with(('gtk-', 'gnome-', 'kde-', 'gtk2-')))
def _name_starts_with(self, words: tuple):
for word in words:
if self.name.startswith(word):
return True
return False
def get_disk_cache_path(self):
return super(SnapApplication, self).get_disk_cache_path() + '/installed/' + self.name
def get_data_to_cache(self):
return {
"icon_url": self.icon_url,
'confinement': self.confinement,
'description': self.description
}
def fill_cached_data(self, data: dict):
if data:
for base_attr in ('icon_url', 'description'):
if data.get(base_attr):
setattr(self, base_attr, data[base_attr])
if data.get('confinement'):
self.confinement = data['confinement']
def get_command(self) -> str:
return "snap run " + self.name
def can_be_run(self) -> bool:
return self.installed and self.is_application()
def get_publisher(self):
return self.publisher
def get_custom_supported_actions(self) -> List[PackageAction]:
if self.installed:
return EXTRA_INSTALLED_ACTIONS

View File

@@ -0,0 +1,146 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Capa_1"
x="0px"
y="0px"
width="48"
height="48"
viewBox="0 0 47.999999 47.999999"
xml:space="preserve"
sodipodi:docname="refresh_2.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)"><metadata
id="metadata45"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs43"><linearGradient
inkscape:collect="always"
id="linearGradient540"><stop
style="stop-color:#0088aa;stop-opacity:1;"
offset="0"
id="stop536" /><stop
style="stop-color:#0088aa;stop-opacity:0;"
offset="1"
id="stop538" /></linearGradient><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient540"
id="linearGradient542"
x1="-130.74721"
y1="-214.80638"
x2="-556.49438"
y2="110.01643"
gradientUnits="userSpaceOnUse" /></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1366"
inkscape:window-height="703"
id="namedview41"
showgrid="false"
showborder="false"
inkscape:zoom="1.9374833"
inkscape:cx="165.58246"
inkscape:cy="-10.288149"
inkscape:window-x="0"
inkscape:window-y="260"
inkscape:window-maximized="1"
inkscape:current-layer="Capa_1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<g
id="g8"
transform="matrix(0.10547014,0,0,0.0985161,-1.6940777,0)"
style="fill:url(#linearGradient542);fill-opacity:1">
<g
id="g6"
style="fill:url(#linearGradient542);fill-opacity:1">
<path
d="m 55.323,203.641 c 15.664,0 29.813,-9.405 35.872,-23.854 25.017,-59.604 83.842,-101.61 152.42,-101.61 37.797,0 72.449,12.955 100.23,34.442 l -21.775,3.371 c -7.438,1.153 -13.224,7.054 -14.232,14.512 -1.01,7.454 3.008,14.686 9.867,17.768 l 119.746,53.872 c 5.249,2.357 11.33,1.904 16.168,-1.205 4.83,-3.114 7.764,-8.458 7.796,-14.208 l 0.621,-131.943 c 0.042,-7.506 -4.851,-14.144 -12.024,-16.332 -7.185,-2.188 -14.947,0.589 -19.104,6.837 L 414.403,70.096 C 370.398,26.778 310.1,0 243.615,0 142.806,0 56.133,61.562 19.167,149.06 c -5.134,12.128 -3.84,26.015 3.429,36.987 7.269,10.976 19.556,17.594 32.727,17.594 z"
id="path2"
inkscape:connector-curvature="0"
style="fill:url(#linearGradient542);fill-opacity:1" />
<path
d="m 464.635,301.184 c -7.27,-10.977 -19.558,-17.594 -32.728,-17.594 -15.664,0 -29.813,9.405 -35.872,23.854 -25.018,59.604 -83.843,101.61 -152.42,101.61 -37.798,0 -72.45,-12.955 -100.232,-34.442 l 21.776,-3.369 c 7.437,-1.153 13.223,-7.055 14.233,-14.514 1.009,-7.453 -3.008,-14.686 -9.867,-17.768 L 49.779,285.089 c -5.25,-2.356 -11.33,-1.905 -16.169,1.205 -4.829,3.114 -7.764,8.458 -7.795,14.207 l -0.622,131.943 c -0.042,7.506 4.85,14.144 12.024,16.332 7.185,2.188 14.948,-0.59 19.104,-6.839 l 16.505,-24.805 c 44.004,43.32 104.303,70.098 170.788,70.098 100.811,0 187.481,-61.561 224.446,-149.059 5.137,-12.128 3.843,-26.014 -3.425,-36.987 z"
id="path4"
inkscape:connector-curvature="0"
style="fill:url(#linearGradient542);fill-opacity:1" />
</g>
</g>
<g
id="g10"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g12"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g14"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g16"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g18"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g20"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g22"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g24"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g26"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g28"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g30"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g32"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g34"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g36"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g38"
transform="translate(-16.062155,-439.23)">
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,3 @@
snap.notification.snapd_unavailable=snapd seems not to be enabled. snap packages will not be available.
snap.action.refresh.status=Refreshing
snap.action.refresh.label=Refresh

View File

@@ -0,0 +1,12 @@
snap.info.commands=comandos
snap.info.contact=contacto
snap.info.description=descripción
snap.info.license=licencia
snap.info.license.unset=no está definida
snap.info.revision=revisión
snap.info.tracking=tracking
snap.info.installed=instalado
snap.info.publisher=publicador
snap.notification.snapd_unavailable=snapd no parece estar habilitado. los paquetes snap estarán indisponibles.
snap.action.refresh.status=Actualizando
snap.action.refresh.label=Actualizar

View File

@@ -0,0 +1,12 @@
snap.info.commands=comandos
snap.info.contact=contato
snap.info.description=descrição
snap.info.license=licença
snap.info.license.unset=não definida
snap.info.revision=revisão
snap.info.tracking=tracking
snap.info.installed=instalado
snap.info.publisher=publicador
snap.notification.snapd_unavailable=snapd não parece estar habilitado. os pacotes snap estarão indisponíveis.
snap.action.refresh.status=Atualizando
snap.action.refresh.label=Atualizar

141
bauh/gems/snap/snap.py Normal file
View File

@@ -0,0 +1,141 @@
import re
import subprocess
from typing import List
from bauh.commons.system import new_root_subprocess, run_cmd
BASE_CMD = 'snap'
def is_installed():
version = get_snapd_version()
return False if version is None else True
def get_version():
res = run_cmd('{} --version'.format(BASE_CMD), print_error=False)
return res.split('\n')[0].split(' ')[-1].strip() if res else None
def get_snapd_version():
res = run_cmd('{} --version'.format(BASE_CMD), print_error=False)
if not res:
return None
else:
lines = res.split('\n')
if lines and len(lines) >= 2:
version = lines[1].split(' ')[-1].strip()
return version if version and version.lower() != 'unavailable' else None
else:
return None
def app_str_to_json(app: str) -> dict:
app_data = [word for word in app.split(' ') if word]
app_json = {
'name': app_data[0],
'version': app_data[1],
'rev': app_data[2],
'tracking': app_data[3],
'publisher': app_data[4] if len(app_data) >= 5 else None,
'notes': app_data[5] if len(app_data) >= 6 else None
}
app_json.update(get_info(app_json['name'], ('summary', 'type', 'description')))
return app_json
def get_info(app_name: str, attrs: tuple = None):
full_info_lines = run_cmd('{} info {}'.format(BASE_CMD, app_name))
data = {}
if full_info_lines:
re_attrs = r'\w+' if not attrs else '|'.join(attrs)
info_map = re.findall(r'({}):\s+(.+)'.format(re_attrs), full_info_lines)
for info in info_map:
data[info[0]] = info[1].strip()
if not attrs or 'description' in attrs:
desc = re.findall(r'\|\n+((\s+.+\n+)+)', full_info_lines)
data['description'] = ''.join([w.strip() for w in desc[0][0].strip().split('\n')]).replace('.', '.\n') if desc else None
if not attrs or 'commands' in attrs:
commands = re.findall(r'commands:\s*\n*((\s+-\s.+\s*\n)+)', full_info_lines)
data['commands'] = commands[0][0].replace('-', '').strip().split('\n') if commands else None
return data
def read_installed() -> List[dict]:
res = run_cmd('{} list'.format(BASE_CMD), print_error=False)
apps = []
if res and len(res) > 0:
lines = res.split('\n')
if not lines[0].startswith('error'):
for idx, app_str in enumerate(lines):
if idx > 0 and app_str:
apps.append(app_str_to_json(app_str))
return apps
def search(word: str, exact_name: bool = False) -> List[dict]:
apps = []
res = run_cmd('{} find "{}"'.format(BASE_CMD, word), print_error=False)
if res:
res = res.split('\n')
if not res[0].startswith('No matching'):
for idx, app_str in enumerate(res):
if idx > 0 and app_str:
app_data = [word for word in app_str.split(' ') if word]
if exact_name and app_data[0] != word:
continue
apps.append({
'name': app_data[0],
'version': app_data[1],
'publisher': app_data[2],
'notes': app_data[3] if app_data[3] != '-' else None,
'summary': app_data[4] if len(app_data) == 5 else '',
'rev': None,
'tracking': None,
'type': None
})
if exact_name and len(apps) > 0:
break
return apps
def uninstall_and_stream(app_name: str, root_password: str):
return new_root_subprocess([BASE_CMD, 'remove', app_name], root_password)
def install_and_stream(app_name: str, confinement: str, root_password: str) -> subprocess.Popen:
install_cmd = [BASE_CMD, 'install', app_name] # default
if confinement == 'classic':
install_cmd.append('--classic')
return new_root_subprocess(install_cmd, root_password)
def downgrade_and_stream(app_name: str, root_password: str) -> subprocess.Popen:
return new_root_subprocess([BASE_CMD, 'revert', app_name], root_password)
def refresh_and_stream(app_name: str, root_password: str) -> subprocess.Popen:
return new_root_subprocess([BASE_CMD, 'refresh', app_name], root_password)

View File

@@ -0,0 +1,13 @@
from bauh.api.abstract.model import SuggestionPriority
ALL = {
'whatsdesk': SuggestionPriority.TOP,
'slack': SuggestionPriority.HIGH,
'discord': SuggestionPriority.HIGH,
'yakyak': SuggestionPriority.MEDIUM,
'instagraph': SuggestionPriority.MEDIUM,
'gimp': SuggestionPriority.MEDIUM,
'pycharm-professional': SuggestionPriority.LOW,
'eclipse': SuggestionPriority.LOW,
'supertuxkart': SuggestionPriority.LOW
}

71
bauh/gems/snap/worker.py Normal file
View File

@@ -0,0 +1,71 @@
import traceback
from threading import Thread
from bauh.api.abstract.cache import MemoryCache
from bauh.api.abstract.context import ApplicationContext
from bauh.api.abstract.controller import SoftwareManager
from bauh.api.abstract.model import PackageStatus
from bauh.gems.snap import snap
from bauh.gems.snap.constants import SNAP_API_URL
from bauh.gems.snap.model import SnapApplication
class SnapAsyncDataLoader(Thread):
def __init__(self, app: SnapApplication, manager: SoftwareManager, api_cache: MemoryCache,
context: ApplicationContext):
super(SnapAsyncDataLoader, self).__init__(daemon=True)
self.app = app
self.id_ = '{}#{}'.format(self.__class__.__name__, id(self))
self.manager = manager
self.http_client = context.http_client
self.api_cache = api_cache
self.persist = False
self.download_icons = context.download_icons
self.logger = context.logger
def run(self):
if self.app:
self.app.status = PackageStatus.LOADING_DATA
try:
res = self.http_client.session.get('{}/search?q={}'.format(SNAP_API_URL, self.app.name))
if res:
try:
snap_list = res.json()['_embedded']['clickindex:package']
except:
self.logger.warning('Snap API response responded differently from expected for app: {}'.format(self.app.name))
return
if not snap_list:
self.logger.warning("Could not retrieve app data for id '{}'. Server response: {}. Body: {}".format(self.app.id, res.status_code, res.content.decode()))
else:
snap_data = snap_list[0]
api_data = {
'confinement': snap_data.get('confinement'),
'description': snap_data.get('description'),
'icon_url': snap_data.get('icon_url') if self.download_icons else None
}
self.api_cache.add(self.app.id, api_data)
self.app.confinement = api_data['confinement']
self.app.icon_url = api_data['icon_url']
if not api_data.get('description'):
api_data['description'] = snap.get_info(self.app.name, ('description',)).get('description')
self.app.description = api_data['description']
self.persist = self.app.supports_disk_cache()
else:
self.logger.warning("Could not retrieve app data for id '{}'. Server response: {}. Body: {}".format(self.app.id, res.status_code, res.content.decode()))
except:
self.logger.error("Could not retrieve app data for id '{}'".format(self.app.id))
traceback.print_exc()
self.app.status = PackageStatus.READY
if self.persist:
self.manager.cache_to_disk(pkg=self.app, icon_bytes=None, only_icon=False)