mirror of
https://github.com/spalencsar/bearhub.git
synced 2026-07-08 01:34:15 +02:00
gem selector sketch
This commit is contained in:
0
bauh/gems/__init__.py
Normal file
0
bauh/gems/__init__.py
Normal file
4
bauh/gems/arch/__init__.py
Normal file
4
bauh/gems/arch/__init__.py
Normal 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
28
bauh/gems/arch/aur.py
Normal 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)])
|
||||
28
bauh/gems/arch/confirmation.py
Normal file
28
bauh/gems/arch/confirmation.py
Normal 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)
|
||||
570
bauh/gems/arch/controller.py
Normal file
570
bauh/gems/arch/controller.py
Normal 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
179
bauh/gems/arch/disk.py
Normal 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
29
bauh/gems/arch/git.py
Normal 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
28
bauh/gems/arch/makepkg.py
Normal 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
55
bauh/gems/arch/mapper.py
Normal 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
20
bauh/gems/arch/message.py
Normal 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
109
bauh/gems/arch/model.py
Normal 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
131
bauh/gems/arch/pacman.py
Normal 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
|
||||
13
bauh/gems/arch/pkgbuild.py
Normal file
13
bauh/gems/arch/pkgbuild.py
Normal 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))
|
||||
BIN
bauh/gems/arch/resources/img/arch.png
Executable file
BIN
bauh/gems/arch/resources/img/arch.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 905 B |
49
bauh/gems/arch/resources/locale/en
Normal file
49
bauh/gems/arch/resources/locale/en
Normal 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.
|
||||
81
bauh/gems/arch/resources/locale/es
Normal file
81
bauh/gems/arch/resources/locale/es
Normal 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.
|
||||
81
bauh/gems/arch/resources/locale/pt
Normal file
81
bauh/gems/arch/resources/locale/pt
Normal 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.
|
||||
3
bauh/gems/arch/suggestions.py
Normal file
3
bauh/gems/arch/suggestions.py
Normal 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
53
bauh/gems/arch/worker.py
Normal 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))
|
||||
3
bauh/gems/flatpak/__init__.py
Normal file
3
bauh/gems/flatpak/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
import os
|
||||
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
2
bauh/gems/flatpak/constants.py
Normal file
2
bauh/gems/flatpak/constants.py
Normal file
@@ -0,0 +1,2 @@
|
||||
FLATHUB_URL = 'https://flathub.org'
|
||||
FLATHUB_API_URL = FLATHUB_URL + '/api/v1'
|
||||
208
bauh/gems/flatpak/controller.py
Normal file
208
bauh/gems/flatpak/controller.py
Normal 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
245
bauh/gems/flatpak/flatpak.py
Executable 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())
|
||||
69
bauh/gems/flatpak/model.py
Normal file
69
bauh/gems/flatpak/model.py
Normal 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'
|
||||
179
bauh/gems/flatpak/resources/img/flathub.svg
Executable file
179
bauh/gems/flatpak/resources/img/flathub.svg
Executable 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 |
BIN
bauh/gems/flatpak/resources/img/flatpak.png
Executable file
BIN
bauh/gems/flatpak/resources/img/flatpak.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 8.7 KiB |
5
bauh/gems/flatpak/resources/locale/en
Normal file
5
bauh/gems/flatpak/resources/locale/en
Normal 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
|
||||
27
bauh/gems/flatpak/resources/locale/es
Normal file
27
bauh/gems/flatpak/resources/locale/es
Normal 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
|
||||
27
bauh/gems/flatpak/resources/locale/pt
Normal file
27
bauh/gems/flatpak/resources/locale/pt
Normal 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
|
||||
15
bauh/gems/flatpak/suggestions.py
Normal file
15
bauh/gems/flatpak/suggestions.py
Normal 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
|
||||
}
|
||||
|
||||
96
bauh/gems/flatpak/worker.py
Normal file
96
bauh/gems/flatpak/worker.py
Normal 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()
|
||||
2
bauh/gems/snap/__init__.py
Normal file
2
bauh/gems/snap/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
import os
|
||||
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
1
bauh/gems/snap/constants.py
Normal file
1
bauh/gems/snap/constants.py
Normal file
@@ -0,0 +1 @@
|
||||
SNAP_API_URL = 'https://search.apps.ubuntu.com/api/v1'
|
||||
145
bauh/gems/snap/controller.py
Normal file
145
bauh/gems/snap/controller.py
Normal 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
88
bauh/gems/snap/model.py
Normal 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
|
||||
146
bauh/gems/snap/resources/img/refresh.svg
Executable file
146
bauh/gems/snap/resources/img/refresh.svg
Executable 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 |
BIN
bauh/gems/snap/resources/img/snap.png
Executable file
BIN
bauh/gems/snap/resources/img/snap.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
3
bauh/gems/snap/resources/locale/en
Normal file
3
bauh/gems/snap/resources/locale/en
Normal 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
|
||||
12
bauh/gems/snap/resources/locale/es
Normal file
12
bauh/gems/snap/resources/locale/es
Normal 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
|
||||
12
bauh/gems/snap/resources/locale/pt
Normal file
12
bauh/gems/snap/resources/locale/pt
Normal 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
141
bauh/gems/snap/snap.py
Normal 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)
|
||||
13
bauh/gems/snap/suggestions.py
Normal file
13
bauh/gems/snap/suggestions.py
Normal 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
71
bauh/gems/snap/worker.py
Normal 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)
|
||||
Reference in New Issue
Block a user