project renamed as 'bauh'

This commit is contained in:
Vinicius Moreira
2019-08-12 11:50:24 -03:00
parent 6d891683da
commit 19a11434fe
69 changed files with 202 additions and 321 deletions

View File

View File

@@ -0,0 +1,5 @@
from bauh.core.constants import CACHE_PATH
FLATHUB_URL = 'https://flathub.org'
FLATHUB_API_URL = FLATHUB_URL + '/api/v1'
FLATPAK_CACHE_PATH = '{}/flatpak/installed'.format(CACHE_PATH)

View File

@@ -0,0 +1,205 @@
import os
import shutil
from argparse import Namespace
from datetime import datetime
from typing import List, Dict
from bauh.core import disk
from bauh.core.controller import ApplicationManager
from bauh.core.disk import DiskCacheLoader
from bauh.core.flatpak import flatpak
from bauh.core.flatpak.model import FlatpakApplication
from bauh.core.flatpak.worker import FlatpakAsyncDataLoader, FlatpakUpdateLoader
from bauh.core.model import ApplicationData, ApplicationUpdate
from bauh.core.system import BauhProcess
from bauh.util.cache import Cache
class FlatpakManager(ApplicationManager):
def __init__(self, app_args: Namespace, api_cache: Cache, disk_cache: bool, http_session, locale_keys: dict):
super(FlatpakManager, self).__init__(app_args=app_args, locale_keys=locale_keys)
self.api_cache = api_cache
self.http_session = http_session
self.disk_cache = disk_cache
def get_app_type(self):
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'),
base_data=ApplicationData(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.add(app) # preloading cached disk data
FlatpakAsyncDataLoader(app=app, api_cache=self.api_cache, manager=self, http_session=self.http_session).start()
else:
app.fill_cached_data(api_data)
return app
def search(self, word: str, disk_loader: DiskCacheLoader) -> Dict[str, List[FlatpakApplication]]:
res = {'installed': [], 'new': []}
apps_found = flatpak.search(word)
if apps_found:
already_read = set()
installed_apps = self.read_installed(disk_loader=disk_loader)
if installed_apps:
for app_found in apps_found:
for installed_app in installed_apps:
if app_found['id'] == installed_app.base_data.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))
return res
def read_installed(self, disk_loader: DiskCacheLoader) -> List[FlatpakApplication]:
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 models
def can_downgrade(self):
return True
def downgrade_app(self, app: FlatpakApplication, root_password: str) -> BauhProcess:
commits = flatpak.get_app_commits(app.ref, app.origin)
commit_idx = commits.index(app.commit)
# downgrade is not possible if the app current commit in the first one:
if commit_idx == len(commits) - 1:
return None
return BauhProcess(subproc=flatpak.downgrade_and_stream(app.ref, commits[commit_idx + 1], root_password),
success_phrase='Updates complete.')
def clean_cache_for(self, app: FlatpakApplication):
self.api_cache.delete(app.base_data.id)
if app.supports_disk_cache() and os.path.exists(app.get_disk_cache_path()):
shutil.rmtree(app.get_disk_cache_path())
def update_and_stream(self, app: FlatpakApplication) -> BauhProcess:
return BauhProcess(subproc=flatpak.update_and_stream(app.ref))
def uninstall_and_stream(self, app: FlatpakApplication, root_password: str = None) -> BauhProcess:
return BauhProcess(subproc=flatpak.uninstall_and_stream(app.ref))
def get_info(self, app: FlatpakApplication) -> dict:
app_info = flatpak.get_app_info_fields(app.base_data.id, app.branch)
app_info['name'] = app.base_data.name
app_info['type'] = 'runtime' if app.runtime else 'app'
app_info['description'] = app.base_data.description
return app_info
def get_history(self, app: FlatpakApplication) -> List[dict]:
return flatpak.get_app_commits_data(app.ref, app.origin)
def install_and_stream(self, app: FlatpakApplication, root_password: str) -> BauhProcess:
return BauhProcess(subproc=flatpak.install_and_stream(app.base_data.id, app.origin))
def is_enabled(self):
return flatpak.is_installed()
def cache_to_disk(self, app: FlatpakApplication, icon_bytes: bytes, only_icon: bool):
if self.disk_cache and app.supports_disk_cache():
disk.save(app, icon_bytes, only_icon)
def requires_root(self, action: str, app: FlatpakApplication):
return action == 'downgrade'
def refresh(self, app: FlatpakApplication, root_password: str) -> BauhProcess:
raise Exception("'refresh' is not supported for {}".format(app.__class__.__name__))
def prepare(self):
pass
def list_updates(self) -> List[ApplicationUpdate]:
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_session=self.http_session)
loader.start()
if loaders is None:
loaders = []
loaders.append(loader)
if loaders:
for loader in loaders:
loader.join()
app = loader.app
updates.append(ApplicationUpdate(app_id='{}:{}'.format(app['id'], app['branch']),
app_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.locale_keys['flatpak.notification.no_remotes']]
def list_suggestions(self, limit: int) -> List[FlatpakApplication]:
res = []
if limit != 0:
for app_id in ('com.spotify.Client', 'com.skype.Client', 'com.dropbox.Client', 'us.zoom.Zoom', 'com.visualstudio.code', 'org.telegram.desktop', 'org.inkscape.Inkscape', 'org.libretro.RetroArch', 'org.kde.kdenlive', 'org.videolan.VLC'):
app_json = flatpak.search(app_id, app_id=True)
if app_json:
res.append(self._map_to_model(app_json[0], False, None))
if len(res) == limit:
break
return res

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

@@ -0,0 +1,245 @@
import re
import subprocess
from typing import List
from bauh.core import system
from bauh.core.exception import NoInternetException
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 = system.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 system.run_cmd('{} info {} {}'.format(BASE_CMD, app_id, branch))
def list_installed(extra_fields: bool = True) -> List[dict]:
apps_str = system.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_and_stream(app_ref: str):
"""
Updates the app reference and streams Flatpak output,
:param app_ref:
:return:
"""
return system.cmd_to_subprocess([BASE_CMD, 'update', '-y', app_ref])
def uninstall_and_stream(app_ref: str):
"""
Removes the app by its reference
:param app_ref:
:return:
"""
return system.cmd_to_subprocess([BASE_CMD, 'uninstall', app_ref, '-y'])
def list_updates_as_str():
return system.run_cmd('{} update'.format(BASE_CMD), ignore_return_code=True)
def downgrade_and_stream(app_ref: str, commit: str, root_password: str) -> subprocess.Popen:
return system.cmd_as_root([BASE_CMD, 'update', '--commit={}'.format(commit), app_ref, '-y'], root_password)
def get_app_commits(app_ref: str, origin: str) -> List[str]:
log = system.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 = system.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 = system.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_and_stream(app_id: str, origin: str):
return system.cmd_to_subprocess([BASE_CMD, 'install', origin, app_id, '-y'])
def set_default_remotes():
system.run_cmd('flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo')
def has_remotes_set() -> bool:
return bool(system.run_cmd('{} remotes'.format(BASE_CMD)).strip())

View File

@@ -0,0 +1,62 @@
from bauh.core import resource
from bauh.core.flatpak.constants import FLATPAK_CACHE_PATH
from bauh.core.model import Application, ApplicationData
class FlatpakApplication(Application):
def __init__(self, base_data: ApplicationData, branch: str, arch: str, origin: str, runtime: bool, ref: str, commit: str):
super(FlatpakApplication, self).__init__(base_data=base_data)
self.ref = ref
self.branch = branch
self.arch = arch
self.origin = origin
self.runtime = runtime
self.commit = commit
def is_incomplete(self):
return self.base_data.description is None and self.base_data.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 can_be_uninstalled(self):
return self.installed
def can_be_installed(self):
return not self.installed
def get_type(self):
return 'flatpak'
def can_be_refreshed(self):
return False
def get_default_icon_path(self):
return resource.get_path('img/flathub.svg')
def is_library(self):
return self.runtime
def get_disk_cache_path(self):
return '{}/{}'.format(FLATPAK_CACHE_PATH, self.base_data.id)
def get_data_to_cache(self):
return {
'description': self.base_data.description,
'icon_url': self.base_data.icon_url,
'latest_version': self.base_data.latest_version,
'version': self.base_data.version,
'name': self.base_data.name
}
def fill_cached_data(self, data: dict):
for attr in self.get_data_to_cache().keys():
if not getattr(self.base_data, attr):
setattr(self.base_data, attr, data[attr])

120
bauh/core/flatpak/worker.py Normal file
View File

@@ -0,0 +1,120 @@
import time
import traceback
from threading import Thread
from colorama import Fore
from bauh.core.controller import ApplicationManager
from bauh.core.flatpak import flatpak
from bauh.core.flatpak.constants import FLATHUB_API_URL, FLATHUB_URL
from bauh.core.flatpak.model import FlatpakApplication
from bauh.core.model import ApplicationStatus
from bauh.core.worker import AsyncDataLoader
from bauh.util.cache import Cache
class FlatpakAsyncDataLoader(AsyncDataLoader):
def __init__(self, app: FlatpakApplication, manager: ApplicationManager, http_session, api_cache: Cache, attempts: int = 2, timeout: int = 30):
super(FlatpakAsyncDataLoader, self).__init__(app=app)
self.manager = manager
self.http_session = http_session
self.attempts = attempts
self.api_cache = api_cache
self.persist = False
self.timeout = timeout
def run(self):
if self.app:
self.app.status = ApplicationStatus.LOADING_DATA
for _ in range(0, self.attempts):
try:
res = self.http_session.get('{}/apps/{}'.format(FLATHUB_API_URL, self.app.base_data.id), timeout=self.timeout)
if res.status_code == 200 and res.text:
data = res.json()
if not self.app.base_data.version:
self.app.base_data.version = data.get('version')
if not self.app.base_data.name:
self.app.base_data.name = data.get('name')
self.app.base_data.description = data.get('description', data.get('summary', None))
self.app.base_data.icon_url = data.get('iconMobileUrl', None)
self.app.base_data.latest_version = data.get('currentReleaseVersion', self.app.base_data.version)
if not self.app.base_data.version and self.app.base_data.latest_version:
self.app.base_data.version = self.app.base_data.latest_version
if not self.app.installed and self.app.base_data.latest_version:
self.app.base_data.version = self.app.base_data.latest_version
if self.app.base_data.icon_url and self.app.base_data.icon_url.startswith('/'):
self.app.base_data.icon_url = FLATHUB_URL + self.app.base_data.icon_url
loaded_data = self.app.get_data_to_cache()
self.api_cache.add(self.app.base_data.id, loaded_data)
self.app.status = ApplicationStatus.READY
self.persist = self.app.supports_disk_cache()
break
else:
self.log_msg("Could not retrieve app data for id '{}'. Server response: {}. Body: {}".format(
self.app.base_data.id, res.status_code, res.content.decode()), Fore.RED)
except:
self.log_msg("Could not retrieve app data for id '{}'".format(self.app.base_data.id), Fore.YELLOW)
traceback.print_exc()
time.sleep(0.5)
self.app.status = ApplicationStatus.READY
if self.persist:
self.manager.cache_to_disk(app=self.app, icon_bytes=None, only_icon=False)
def clone(self) -> "FlatpakAsyncDataLoader":
return FlatpakAsyncDataLoader(manager=self.manager,
api_cache=self.api_cache,
attempts=self.attempts,
http_session=self.http_session,
timeout=self.timeout,
app=self.app)
class FlatpakUpdateLoader(Thread):
def __init__(self, app: dict, http_session, attempts: int = 2, timeout: int = 20):
super(FlatpakUpdateLoader, self).__init__(daemon=True)
self.app = app
self.http_session = http_session
self.attempts = attempts
self.timeout = timeout
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']:
current_attempts = 0
while current_attempts < self.attempts:
current_attempts += 1
try:
res = self.http_session.get('{}/apps/{}'.format(FLATHUB_API_URL, self.app['id']), timeout=self.timeout)
if res.status_code == 200 and res.text:
data = res.json()
if data.get('currentReleaseVersion'):
self.app['version'] = data['currentReleaseVersion']
break
except:
traceback.print_exc()
time.sleep(0.5)