supporting snaps

This commit is contained in:
Vinícius Moreira
2019-07-23 17:16:44 -03:00
committed by GitHub
parent 332d3cb787
commit 854ad2a102
39 changed files with 1739 additions and 650 deletions

View File

@@ -1,2 +1,6 @@
FLATHUB_URL = 'https://flathub.org'
FLATHUB_API_URL = FLATHUB_URL + '/api/v1'
from pathlib import Path
from fpakman import __app_name__
HOME_PATH = Path.home()
CACHE_PATH = '{}/.cache/{}'.format(HOME_PATH, __app_name__)

View File

@@ -1,30 +1,29 @@
import os
import shutil
import time
from abc import ABC, abstractmethod
from datetime import datetime
from argparse import Namespace
from threading import Lock
from typing import List
from typing import List, Dict
import requests
from fpakman.core import flatpak, disk
from fpakman.core.disk import DiskCacheLoader, DiskCacheLoaderFactory
from fpakman.core.model import FlatpakApplication, ApplicationData, ApplicationStatus, Application
from fpakman.util.cache import Cache
from fpakman.core.model import Application
from fpakman.core.system import FpakmanProcess
class ApplicationManager(ABC):
def __init__(self, app_args):
self.app_args = app_args
@abstractmethod
def search(self, word: str, disk_loader: DiskCacheLoader) -> List[Application]:
def search(self, word: str, disk_loader: DiskCacheLoader) -> Dict[str, List[Application]]:
pass
@abstractmethod
def read_installed(self, disk_loader: DiskCacheLoader, keep_workers: bool) -> List[Application]:
def read_installed(self, disk_loader: DiskCacheLoader) -> List[Application]:
pass
@abstractmethod
def downgrade_app(self, app: Application, root_password: str):
def downgrade_app(self, app: Application, root_password: str) -> FpakmanProcess:
pass
@abstractmethod
@@ -36,15 +35,15 @@ class ApplicationManager(ABC):
pass
@abstractmethod
def update_and_stream(self, app: Application):
def update_and_stream(self, app: Application) -> FpakmanProcess:
pass
@abstractmethod
def uninstall_and_stream(self, app: Application):
def uninstall_and_stream(self, app: Application, root_password: str) -> FpakmanProcess:
pass
@abstractmethod
def get_app_type(self) -> str:
def get_app_type(self):
pass
@abstractmethod
@@ -56,7 +55,7 @@ class ApplicationManager(ABC):
pass
@abstractmethod
def install_and_stream(self, app: Application):
def install_and_stream(self, app: Application, root_password: str) -> FpakmanProcess:
pass
@abstractmethod
@@ -67,194 +66,127 @@ class ApplicationManager(ABC):
def cache_to_disk(self, app: Application, icon_bytes: bytes, only_icon: bool):
pass
@abstractmethod
def requires_root(self, action: str, app: Application):
pass
from fpakman.core.worker import FlatpakAsyncDataLoaderManager
@abstractmethod
def refresh(self, app: Application, root_password: str) -> FpakmanProcess:
pass
@abstractmethod
def prepare(self):
"""
Callback executed before the ApplicationManager starts to work.
:return:
"""
pass
class FlatpakManager(ApplicationManager):
class GenericApplicationManager(ApplicationManager):
def __init__(self, api_cache: Cache, disk_cache: bool):
self.api_cache = api_cache
self.http_session = requests.Session()
def __init__(self, managers: List[ApplicationManager], disk_loader_factory: DiskCacheLoaderFactory, app_args: Namespace):
super(ApplicationManager, self).__init__()
self.managers = managers
self.map = {m.get_app_type(): m for m in self.managers}
self.disk_loader_factory = disk_loader_factory
self.lock_read = Lock()
self.disk_cache = disk_cache
self.async_data_loader = FlatpakAsyncDataLoaderManager(api_cache=self.api_cache, manager=self)
flatpak.set_default_remotes()
self._enabled_map = {} if app_args.check_packaging_once else None
self.prepare()
def get_app_type(self):
return FlatpakApplication
def _sort(self, apps: List[Application], word: str) -> List[Application]:
def _map_to_model(self, app: dict, installed: bool, disk_loader: DiskCacheLoader) -> FlatpakApplication:
exact_name_matches, contains_name_matches, desc_name_matches, others = [], [], [], []
model = FlatpakApplication(arch=app.get('arch'),
branch=app.get('branch'),
origin=app.get('origin'),
runtime=app.get('runtime'),
ref=app.get('ref'),
commit=app.get('commit'),
base_data=ApplicationData(id=app.get('id'),
name=app.get('name'),
version=app.get('version'),
latest_version=app.get('latest_version')))
model.installed = installed
for app in apps:
lower_name = app.base_data.name.lower()
api_data = self.api_cache.get(app['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['runtime']:
disk_loader.add(model) # preloading cached disk data
model.status = ApplicationStatus.LOADING_DATA
self.async_data_loader.load(model)
else:
model.fill_cached_data(api_data)
return model
def search(self, word: str, disk_loader: DiskCacheLoader) -> List[FlatpakApplication]:
if word == lower_name:
exact_name_matches.append(app)
elif word in lower_name:
contains_name_matches.append(app)
elif app.base_data.description and word in app.base_data.description.lower():
desc_name_matches.append(app)
else:
others.append(app)
res = []
apps_found = flatpak.search(word)
if apps_found:
already_read = set()
installed_apps = self.read_installed(disk_loader=disk_loader, keep_workers=True)
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.append(installed_app)
already_read.add(app_found['id'])
for app_found in apps_found:
if app_found['id'] not in already_read:
res.append(self._map_to_model(app_found, False, disk_loader))
disk_loader.stop = True
disk_loader.join()
self.async_data_loader.stop_current_workers()
for app_list in (exact_name_matches, contains_name_matches, desc_name_matches, others):
app_list.sort(key=lambda a: a.base_data.name.lower())
res.extend(app_list)
return res
def read_installed(self, disk_loader: DiskCacheLoader, keep_workers: bool = False) -> List[FlatpakApplication]:
def _is_enabled(self, man: ApplicationManager):
if self._enabled_map is not None:
enabled = self._enabled_map.get(man.get_app_type())
if enabled is None:
enabled = man.is_enabled()
self._enabled_map[man.get_app_type()] = enabled
return enabled
else:
return man.is_enabled()
def search(self, word: str, disk_loader: DiskCacheLoader = None) -> Dict[str, List[Application]]:
self.lock_read.acquire()
try:
res = {'installed': [], 'new': []}
norm_word = word.strip().lower()
disk_loader = None
for man in self.managers:
if self._is_enabled(man):
if not disk_loader:
disk_loader = self.disk_loader_factory.new()
disk_loader.start()
apps_found = man.search(word=norm_word, disk_loader=disk_loader)
res['installed'].extend(apps_found['installed'])
res['new'].extend(apps_found['new'])
disk_loader.stop = True
disk_loader.join()
for key in res:
res[key] = self._sort(res[key], norm_word)
return res
finally:
self.lock_read.release()
def read_installed(self, disk_loader: DiskCacheLoader = None) -> List[Application]:
self.lock_read.acquire()
try:
installed = flatpak.list_installed()
installed = []
if installed:
installed.sort(key=lambda p: p['name'].lower())
disk_loader = None
available_updates = flatpak.list_updates_as_str()
for man in self.managers:
if self._is_enabled(man):
if not disk_loader:
disk_loader = self.disk_loader_factory.new()
disk_loader.start()
models = []
installed.extend(man.read_installed(disk_loader=disk_loader))
for app in installed:
model = self._map_to_model(app, True, disk_loader)
model.update = app['id'] in available_updates
models.append(model)
disk_loader.stop = True
disk_loader.join()
if not keep_workers:
self.async_data_loader.stop_current_workers()
return models
return []
installed.sort(key=lambda a: a.base_data.name.lower())
return installed
finally:
self.lock_read.release()
def can_downgrade(self):
return True
def downgrade_app(self, app: FlatpakApplication, root_password: str):
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 flatpak.downgrade_and_stream(app.ref, commits[commit_idx + 1], root_password)
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):
return flatpak.update_and_stream(app.ref)
def uninstall_and_stream(self, app: FlatpakApplication):
return 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):
return 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)
class GenericApplicationManager(ApplicationManager):
def __init__(self, managers: List[ApplicationManager], disk_loader_factory: DiskCacheLoaderFactory):
self.managers = managers
self.map = {m.get_app_type(): m for m in self.managers}
self.disk_loader_factory = disk_loader_factory
def search(self, word: str, disk_loader: DiskCacheLoader = None) -> List[Application]:
apps = []
disk_loader = self.disk_loader_factory.new()
disk_loader.start()
for man in self.managers:
if man.is_enabled():
apps.extend(man.search(word, disk_loader))
disk_loader.stop = True
disk_loader.join()
return apps
def read_installed(self, disk_loader: DiskCacheLoader = None, keep_workers: bool = False) -> List[Application]:
installed = []
disk_loader = self.disk_loader_factory.new()
disk_loader.start()
for man in self.managers:
if man.is_enabled():
installed.extend(man.read_installed(disk_loader=disk_loader, keep_workers=keep_workers))
disk_loader.stop = True
disk_loader.join()
return installed
def can_downgrade(self):
return True
def downgrade_app(self, app: Application, root_password: str):
def downgrade_app(self, app: Application, root_password: str) -> FpakmanProcess:
man = self._get_manager_for(app)
if man and man.can_downgrade():
@@ -268,23 +200,23 @@ class GenericApplicationManager(ApplicationManager):
if man:
return man.clean_cache_for(app)
def update_and_stream(self, app: Application):
def update_and_stream(self, app: Application) -> FpakmanProcess:
man = self._get_manager_for(app)
if man:
return man.update_and_stream(app)
def uninstall_and_stream(self, app: Application):
def uninstall_and_stream(self, app: Application, root_password: str) -> FpakmanProcess:
man = self._get_manager_for(app)
if man:
return man.uninstall_and_stream(app)
return man.uninstall_and_stream(app, root_password)
def install_and_stream(self, app: Application):
def install_and_stream(self, app: Application, root_password: str) -> FpakmanProcess:
man = self._get_manager_for(app)
if man:
return man.install_and_stream(app)
return man.install_and_stream(app, root_password)
def get_info(self, app: Application):
man = self._get_manager_for(app)
@@ -306,7 +238,7 @@ class GenericApplicationManager(ApplicationManager):
def _get_manager_for(self, app: Application) -> ApplicationManager:
man = self.map[app.__class__]
return man if man and man.is_enabled() else None
return man if man and self._is_enabled(man) else None
def cache_to_disk(self, app: Application, icon_bytes: bytes, only_icon: bool):
if self.disk_loader_factory.disk_cache and app.supports_disk_cache():
@@ -314,3 +246,22 @@ class GenericApplicationManager(ApplicationManager):
if man:
return man.cache_to_disk(app, icon_bytes=icon_bytes, only_icon=only_icon)
def requires_root(self, action: str, app: Application):
man = self._get_manager_for(app)
if man:
return man.requires_root(action, app)
def refresh(self, app: Application, root_password: str) -> FpakmanProcess:
man = self._get_manager_for(app)
if man:
return man.refresh(app, root_password)
def prepare(self):
if self.managers:
for man in self.managers:
if self._is_enabled(man):
man.prepare()

View File

@@ -2,20 +2,20 @@ import json
import os
from pathlib import Path
from threading import Thread, Lock
from typing import List
from typing import List, Dict
from fpakman.core.model import Application, FlatpakApplication
from fpakman.core.model import Application
from fpakman.util.cache import Cache
class DiskCacheLoader(Thread):
def __init__(self, enabled: bool, flatpak_api_cache: Cache, apps: List[Application] = []):
def __init__(self, enabled: bool, cache_map: Dict[type, Cache], apps: List[Application] = []):
super(DiskCacheLoader, self).__init__(daemon=True)
self.apps = apps
self.stop = False
self.lock = Lock()
self.flatpak_api_cache = flatpak_api_cache
self.cache_map = cache_map
self.enabled = enabled
def run(self):
@@ -44,19 +44,17 @@ class DiskCacheLoader(Thread):
with open(app.get_disk_data_path()) as f:
cached_data = json.loads(f.read())
app.fill_cached_data(cached_data)
if isinstance(app, FlatpakApplication):
self.flatpak_api_cache.add_non_existing(app.base_data.id, cached_data)
self.cache_map.get(app.__class__).add_non_existing(app.base_data.id, cached_data)
class DiskCacheLoaderFactory:
def __init__(self, disk_cache: bool, flatpak_api_cache: Cache):
def __init__(self, disk_cache: bool, cache_map: Dict[type, Cache]):
self.disk_cache = disk_cache
self.flatpak_api_cache = flatpak_api_cache
self.cache_map = cache_map
def new(self):
return DiskCacheLoader(enabled=self.disk_cache, flatpak_api_cache=self.flatpak_api_cache)
return DiskCacheLoader(enabled=self.disk_cache, cache_map=self.cache_map)
def save(app: Application, icon_bytes: bytes = None, only_icon: bool = False):
@@ -65,12 +63,10 @@ def save(app: Application, icon_bytes: bytes = None, only_icon: bool = False):
if not only_icon:
Path(app.get_disk_cache_path()).mkdir(parents=True, exist_ok=True)
data = app.get_data_to_cache()
if isinstance(app, FlatpakApplication):
data = app.get_data_to_cache()
with open(app.get_disk_data_path(), 'w+') as f:
f.write(json.dumps(data))
with open(app.get_disk_data_path(), 'w+') as f:
f.write(json.dumps(data))
if icon_bytes:
Path(app.get_disk_cache_path()).mkdir(parents=True, exist_ok=True)

View File

View File

@@ -0,0 +1,5 @@
from fpakman.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,150 @@
import os
import shutil
from argparse import Namespace
from datetime import datetime
from typing import List, Dict
from fpakman.core import disk
from fpakman.core.controller import ApplicationManager
from fpakman.core.disk import DiskCacheLoader
from fpakman.core.flatpak import flatpak
from fpakman.core.flatpak.model import FlatpakApplication
from fpakman.core.flatpak.worker import FlatpakAsyncDataLoader
from fpakman.core.model import ApplicationData
from fpakman.core.system import FpakmanProcess
from fpakman.util.cache import Cache
class FlatpakManager(ApplicationManager):
def __init__(self, app_args: Namespace, api_cache: Cache, disk_cache: bool, http_session):
super(FlatpakManager, self).__init__(app_args=app_args)
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']:
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) -> FpakmanProcess:
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 FpakmanProcess(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) -> FpakmanProcess:
return FpakmanProcess(subproc=flatpak.update_and_stream(app.ref))
def uninstall_and_stream(self, app: FlatpakApplication, root_password: str = None) -> FpakmanProcess:
return FpakmanProcess(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) -> FpakmanProcess:
return FpakmanProcess(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) -> FpakmanProcess:
raise Exception("'refresh' is not supported for {}".format(app.__class__.__name__))
def prepare(self):
flatpak.set_default_remotes()

View File

@@ -4,7 +4,6 @@ from typing import List
from fpakman.core import system
from fpakman.core.exception import NoInternetException
from fpakman.core.model import Application
BASE_CMD = 'flatpak'
@@ -115,16 +114,8 @@ 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):
pwdin, downgrade_cmd = None, []
if root_password is not None:
downgrade_cmd.extend(['sudo', '-S'])
pwdin = system.stream_cmd(['echo', root_password])
downgrade_cmd.extend([BASE_CMD, 'update', '--commit={}'.format(commit), app_ref, '-y'])
return subprocess.Popen(downgrade_cmd, stdin=pwdin, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout
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]:

View File

@@ -0,0 +1,62 @@
from fpakman.core import resource
from fpakman.core.flatpak.constants import FLATPAK_CACHE_PATH
from fpakman.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 True
def has_info(self):
return self.installed
def can_be_downgraded(self):
return True
def can_be_uninstalled(self):
return True
def can_be_installed(self):
return True
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])

View File

@@ -0,0 +1,84 @@
import time
import traceback
from colorama import Fore
from fpakman.core.controller import ApplicationManager
from fpakman.core.flatpak.constants import FLATHUB_API_URL, FLATHUB_URL
from fpakman.core.flatpak.model import FlatpakApplication
from fpakman.core.model import ApplicationStatus
from fpakman.core.worker import AsyncDataLoader
from fpakman.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.to_persist = {} # stores all data loaded by the instance
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 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
if self.app.supports_disk_cache():
self.to_persist[self.app.base_data.id] = self.app
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
def cache_to_disk(self):
if self.to_persist:
for app in self.to_persist.values():
self.manager.cache_to_disk(app=app, icon_bytes=None, only_icon=False)
self.to_persist = {}
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)

View File

@@ -2,7 +2,6 @@ from abc import ABC, abstractmethod
from enum import Enum
from fpakman.core import resource
from fpakman.core.structure import flatpak_cache_path
class ApplicationStatus(Enum):
@@ -49,6 +48,10 @@ class Application(ABC):
def can_be_installed(self):
pass
@abstractmethod
def can_be_refreshed(self):
return self.installed
@abstractmethod
def get_type(self):
pass
@@ -60,74 +63,12 @@ class Application(ABC):
def is_library(self):
pass
@abstractmethod
def supports_disk_cache(self):
pass
@abstractmethod
def get_disk_cache_path(self):
pass
@abstractmethod
def get_disk_icon_path(self):
pass
@abstractmethod
def get_disk_data_path(self):
pass
@abstractmethod
def get_data_to_cache(self):
pass
@abstractmethod
def fill_cached_data(self, data: dict):
pass
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 True
def has_info(self):
return True
def can_be_downgraded(self):
return not self.runtime
def can_be_uninstalled(self):
return True
def can_be_installed(self):
return True
def get_type(self):
return 'flatpak'
def get_default_icon_path(self):
return resource.get_path('img/flathub.svg')
def is_library(self):
return self.runtime
def supports_disk_cache(self):
return self.installed and not self.is_library()
@abstractmethod
def get_disk_cache_path(self):
return '{}/{}'.format(flatpak_cache_path, self.base_data.id)
pass
def get_disk_icon_path(self):
return '{}/icon.png'.format(self.get_disk_cache_path())
@@ -135,16 +76,13 @@ class FlatpakApplication(Application):
def get_disk_data_path(self):
return '{}/data.json'.format(self.get_disk_cache_path())
@abstractmethod
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
}
pass
@abstractmethod
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])
pass
def __str__(self):
return '{} (id={}, name={})'.format(self.__class__.__name__, self.base_data.id, self.base_data.name)

View File

@@ -1,7 +1,6 @@
import os
app_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
from fpakman import ROOT_DIR
def get_path(resource_path):
return app_dir + '/resources/' + resource_path
return ROOT_DIR + '/resources/' + resource_path

View File

View File

@@ -0,0 +1,4 @@
from fpakman.core.constants import CACHE_PATH
SNAP_STORE_URL = 'https://snapcraft.io'
SNAP_CACHE_PATH = '{}/snap/installed'.format(CACHE_PATH)

View File

@@ -0,0 +1,133 @@
import os
import shutil
from argparse import Namespace
from datetime import datetime
from typing import Dict, List
from fpakman.core import disk
from fpakman.core.controller import ApplicationManager
from fpakman.core.disk import DiskCacheLoader
from fpakman.core.model import ApplicationData, Application
from fpakman.core.snap import snap
from fpakman.core.snap.model import SnapApplication
from fpakman.core.snap.worker import SnapAsyncDataLoader
from fpakman.core.system import FpakmanProcess
from fpakman.util.cache import Cache
class SnapManager(ApplicationManager):
def __init__(self, app_args: Namespace, api_cache: Cache, disk_cache: bool, http_session):
super(SnapManager, self).__init__(app_args=app_args)
self.api_cache = api_cache
self.http_session = http_session
self.disk_cache = disk_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'),
base_data=ApplicationData(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 not app.is_library():
if disk_loader and app.installed:
disk_loader.add(app)
SnapAsyncDataLoader(app=app, api_cache=self.api_cache, manager=self, http_session=self.http_session, download_icons=self.app_args.download_icons).start()
else:
app.fill_cached_data(api_data)
return app
def search(self, word: str, disk_loader: DiskCacheLoader) -> Dict[str, List[SnapApplication]]:
installed = self.read_installed(disk_loader)
res = {'installed': [], 'new': []}
for app_json in snap.search(word):
already_installed = None
if installed:
already_installed = [i for i in installed if i.base_data.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))
return res
def read_installed(self, disk_loader: DiskCacheLoader) -> List[SnapApplication]:
res = [self.map_json(app_json, installed=True, disk_loader=disk_loader) for app_json in snap.read_installed()]
return res
def downgrade_app(self, app: Application, root_password: str) -> FpakmanProcess:
return FpakmanProcess(subproc=snap.downgrade_and_stream(app.base_data.name, root_password), wrong_error_phrase=None)
def clean_cache_for(self, app: SnapApplication):
self.api_cache.delete(app.base_data.name)
if app.supports_disk_cache() and os.path.exists(app.get_disk_cache_path()):
shutil.rmtree(app.get_disk_cache_path())
def can_downgrade(self):
return True
def update_and_stream(self, app: SnapApplication) -> FpakmanProcess:
pass
def uninstall_and_stream(self, app: SnapApplication, root_password: str) -> FpakmanProcess:
return FpakmanProcess(subproc=snap.uninstall_and_stream(app.base_data.name, root_password))
def get_app_type(self):
return SnapApplication
def get_info(self, app: SnapApplication) -> dict:
info = snap.get_info(app.base_data.name, attrs=('license', 'contact', 'commands', 'snap-id', 'tracking', 'installed'))
info['description'] = app.base_data.description
info['publisher'] = app.publisher
info['revision'] = app.rev
info['name'] = app.base_data.name
if info.get('commands'):
info['commands'] = ' '.join(info['commands'])
return info
def get_history(self, app: Application) -> List[dict]:
return []
def install_and_stream(self, app: SnapApplication, root_password: str) -> FpakmanProcess:
return FpakmanProcess(subproc=snap.install_and_stream(app.base_data.name, app.install_cmd, root_password))
def is_enabled(self) -> bool:
return snap.is_installed()
def cache_to_disk(self, app: Application, 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: SnapApplication):
return action != 'search'
def refresh(self, app: SnapApplication, root_password: str) -> FpakmanProcess:
return FpakmanProcess(subproc=snap.refresh_and_stream(app.base_data.name, root_password))
def prepare(self):
pass

View File

@@ -0,0 +1,60 @@
from fpakman.core import resource
from fpakman.core.model import Application, ApplicationData
from fpakman.core.snap.constants import SNAP_CACHE_PATH
class SnapApplication(Application):
def __init__(self, base_data: ApplicationData, publisher: str, rev: str, notes: str, app_type: str, install_cmd: str = None):
super(SnapApplication, self).__init__(base_data=base_data)
self.publisher = publisher
self.rev = rev
self.notes = notes
self.type = app_type
self.install_cmd = install_cmd
def has_history(self):
return False
def has_info(self):
return True
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 can_be_refreshed(self):
return self.installed
def get_type(self):
return 'snap'
def get_default_icon_path(self):
return resource.get_path('img/snapcraft.png')
def is_library(self):
return self.type in ('core', 'base', 'snapd') or self.base_data.name.startswith('gtk-') or self.base_data.name.startswith('gnome-')
def get_disk_cache_path(self):
return '{}/{}'.format(SNAP_CACHE_PATH, self.base_data.name)
def get_data_to_cache(self):
return {
"icon_url": self.base_data.icon_url,
'install_cmd': self.install_cmd,
'description': self.base_data.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_data, base_attr, data[base_attr])
if data.get('install_cmd'):
self.install_cmd = data['install_cmd']

145
fpakman/core/snap/snap.py Normal file
View File

@@ -0,0 +1,145 @@
import re
import subprocess
from typing import List
import requests
from bs4 import BeautifulSoup
from fpakman.core import system
from fpakman.core.snap.constants import SNAP_STORE_URL
BASE_CMD = 'snap'
def is_installed():
version = get_snapd_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('\n')[0].split(' ')[-1].strip() if res else None
def get_snapd_version():
res = system.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],
'notes': app_data[5]
}
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 = system.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 = system.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) -> List[dict]:
apps = []
res = system.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]
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
})
return apps
def uninstall_and_stream(app_name: str, root_password: str):
return system.cmd_as_root([BASE_CMD, 'remove', app_name], root_password)
def install_and_stream(app_name: str, custom_install_cmd: str, root_password: str) -> subprocess.Popen:
install_cmd = [BASE_CMD, 'install', app_name] # default
if custom_install_cmd:
install_cmd = custom_install_cmd.split(' ')
else: # tries to retrieve the snapstore proper installation command
res = requests.get('{}/{}'.format(SNAP_STORE_URL, app_name))
if res.status_code == 200:
soup = BeautifulSoup(res.text, 'html.parser')
input_install_cmd = soup.find("input", {"id": "snap-install"})
install_cmd = input_install_cmd.get("value").strip().split(' ')
return system.cmd_as_root(install_cmd, root_password)
def downgrade_and_stream(app_name: str, root_password: str) -> subprocess.Popen:
return system.cmd_as_root([BASE_CMD, 'revert', app_name], root_password)
def refresh_and_stream(app_name: str, root_password: str) -> subprocess.Popen:
return system.cmd_as_root([BASE_CMD, 'refresh', app_name], root_password)

View File

@@ -0,0 +1,87 @@
import time
import traceback
from bs4 import BeautifulSoup
from colorama import Fore
from fpakman.core.controller import ApplicationManager
from fpakman.core.model import ApplicationStatus
from fpakman.core.snap import snap
from fpakman.core.snap.constants import SNAP_STORE_URL
from fpakman.core.snap.model import SnapApplication
from fpakman.core.worker import AsyncDataLoader
from fpakman.util.cache import Cache
class SnapAsyncDataLoader(AsyncDataLoader):
def __init__(self, app: SnapApplication, manager: ApplicationManager, http_session, api_cache: Cache, download_icons: bool, attempts: int = 2, timeout: int = 30):
super(SnapAsyncDataLoader, self).__init__(app=app)
self.manager = manager
self.http_session = http_session
self.attempts = attempts
self.api_cache = api_cache
self.timeout = timeout
self.to_persist = {} # stores all data loaded by the instance
self.download_icons = download_icons
def run(self):
if self.app:
self.app.status = ApplicationStatus.LOADING_DATA
if not self.app.base_data.description:
self.app.base_data.description = snap.get_info(self.app.base_data.name, ('description',)).get('description')
for _ in range(0, self.attempts):
try:
res = self.http_session.get('{}/{}'.format(SNAP_STORE_URL, self.app.base_data.name),
timeout=self.timeout)
if res.status_code == 200 and res.text:
soup = BeautifulSoup(res.text, 'html.parser')
input_install_cmd = soup.find("input", {"id": "snap-install"})
api_data = {
'install_cmd': input_install_cmd.get("value").strip(),
'description': self.app.base_data.description
}
img_icon = None
if self.download_icons:
img_icon = soup.find('img', {"class": "p-snap-heading__icon"})
if img_icon and img_icon.get("src") and 'snapcraft-missing-icon' not in img_icon.get('src'):
api_data['icon_url'] = img_icon.get("src")
self.api_cache.add(self.app.base_data.id, api_data)
self.app.install_cmd = api_data['install_cmd']
self.app.base_data.icon_url = api_data.get('icon_url')
self.app.status = ApplicationStatus.READY
if self.app.supports_disk_cache():
self.to_persist[self.app.base_data.id] = self.app
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(app.base_data.id), Fore.YELLOW)
traceback.print_exc()
time.sleep(0.5)
self.app.status = ApplicationStatus.READY
def clone(self) -> "SnapAsyncDataLoader":
return SnapAsyncDataLoader(manager=self.manager,
api_cache=self.api_cache,
attempts=self.attempts,
http_session=self.http_session,
timeout=self.timeout,
app=self.app)
def cache_to_disk(self):
if self.to_persist:
for app in self.to_persist.values():
self.manager.cache_to_disk(app=app, icon_bytes=None, only_icon=False)
self.to_persist = {}

View File

@@ -1,12 +0,0 @@
from pathlib import Path
from fpakman import __app_name__
home_path = Path.home()
cache_path = '{}/.cache/{}'.format(home_path, __app_name__)
flatpak_cache_path = '{}/flatpak/installed'.format(cache_path)
def prepare_folder_structure(disk_cache: bool):
if disk_cache:
Path(flatpak_cache_path).mkdir(parents=True, exist_ok=True)

View File

@@ -2,9 +2,18 @@ import os
import subprocess
from typing import List
from fpakman import __app_name__
from fpakman.core import resource
class FpakmanProcess:
def __init__(self, subproc: subprocess.Popen, success_phrase: str = None, wrong_error_phrase: str = '[sudo] password for'):
self.subproc = subproc
self.success_pgrase = success_phrase
self.wrong_error_phrase = wrong_error_phrase
def run_cmd(cmd: str, expected_code: int = 0, ignore_return_code: bool = False, print_error: bool = True) -> str:
args = {
"shell": True,
@@ -28,4 +37,15 @@ def cmd_to_subprocess(cmd: List[str]):
def notify_user(msg: str, icon_path: str = resource.get_path('img/logo.svg')):
os.system("notify-send {} '{}'".format("-i {}".format(icon_path) if icon_path else '', msg))
os.system("notify-send -a {} {} '{}'".format(__app_name__, "-i {}".format(icon_path) if icon_path else '', msg))
def cmd_as_root(cmd: List[str], root_password: str) -> subprocess.Popen:
pwdin, final_cmd = None, []
if root_password is not None:
final_cmd.extend(['sudo', '-S'])
pwdin = stream_cmd(['echo', root_password])
final_cmd.extend(cmd)
return subprocess.Popen(final_cmd, stdin=pwdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

View File

@@ -1,29 +1,17 @@
import traceback
from io import StringIO
from threading import Thread
from typing import List
import requests
from colorama import Fore
from fpakman.core.constants import FLATHUB_API_URL, FLATHUB_URL
from fpakman.core.controller import ApplicationManager
from fpakman.core.model import FlatpakApplication, ApplicationStatus
from fpakman.util.cache import Cache
from fpakman.core.model import Application
class FlatpakAsyncDataLoader(Thread):
class AsyncDataLoader(Thread):
def __init__(self, manager: ApplicationManager, http_session, api_cache: Cache, attempts: int = 3, apps: List[FlatpakApplication] = []):
super(FlatpakAsyncDataLoader, self).__init__(daemon=True)
self.apps = apps
self.http_session = http_session
self.attempts = attempts
self.api_cache = api_cache
def __init__(self, app: Application):
super(AsyncDataLoader, self).__init__(daemon=True)
self.id_ = '{}#{}'.format(self.__class__.__name__, id(self))
self.stop = False
self.to_persist = {} # stores all data loaded by the instance
self.manager = manager
self.app = app
def log_msg(self, msg: str, color: int = None):
final_msg = StringIO()
@@ -41,100 +29,3 @@ class FlatpakAsyncDataLoader(Thread):
final_msg.seek(0)
print(final_msg.read())
def run(self):
while True:
if self.apps:
app = self.apps[0]
app.status = ApplicationStatus.LOADING_DATA
for _ in range(0, self.attempts):
try:
res = self.http_session.get('{}/apps/{}'.format(FLATHUB_API_URL, app.base_data.id), timeout=30)
if res.status_code == 200 and res.text:
data = res.json()
if not app.base_data.version:
app.base_data.version = data.get('version')
if not app.base_data.name:
app.base_data.name = data.get('name')
app.base_data.description = data.get('description', data.get('summary', None))
app.base_data.icon_url = data.get('iconMobileUrl', None)
app.base_data.latest_version = data.get('currentReleaseVersion', app.base_data.version)
if not app.base_data.version and app.base_data.latest_version:
app.base_data.version = app.base_data.latest_version
if app.base_data.icon_url and app.base_data.icon_url.startswith('/'):
app.base_data.icon_url = FLATHUB_URL + app.base_data.icon_url
loaded_data = app.get_data_to_cache()
self.api_cache.add(app.base_data.id, loaded_data)
app.status = ApplicationStatus.READY
if app.supports_disk_cache():
self.to_persist[app.base_data.id] = app
break
else:
self.log_msg("Could not retrieve app data for id '{}'. Server response: {}. Body: {}".format(app.base_data.id, res.status_code, res.content.decode()), Fore.RED)
except:
self.log_msg("Could not retrieve app data for id '{}'".format(app.base_data.id), Fore.YELLOW)
traceback.print_exc()
if self.apps:
del self.apps[0]
elif self.stop:
self.cache_to_disk()
break # stop working
def add(self, app: FlatpakApplication):
self.apps.append(app)
def current_load(self):
return len(self.apps)
def cache_to_disk(self):
if self.to_persist:
for app in self.to_persist.values():
self.manager.cache_to_disk(app=app, icon_bytes=None, only_icon=False)
self.to_persist = {}
class FlatpakAsyncDataLoaderManager:
def __init__(self, manager: ApplicationManager, api_cache: Cache, worker_load: int = 1, workers: List[FlatpakAsyncDataLoader] = []):
self.worker_load = worker_load
self.current_workers = workers
self.http_session = requests.Session()
self.api_cache = api_cache
self.manager = manager
def load(self, app: FlatpakApplication):
available_workers = [w for w in self.current_workers if w.current_load() < self.worker_load]
if available_workers:
worker = available_workers[0]
else: # new worker
worker = FlatpakAsyncDataLoader(http_session=self.http_session,
api_cache=self.api_cache,
manager=self.manager)
worker.start()
self.current_workers.append(worker)
worker.add(app)
def stop_current_workers(self):
for w in self.current_workers:
w.stop = True
self.current_workers = []