From adfb265dc3408379c7ef817a344616b55f9369e6 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Wed, 23 Dec 2020 13:39:58 -0300 Subject: [PATCH] [appimage] improvements -> database files only updated when expired --- CHANGELOG.md | 10 ++ README.md | 12 +- bauh/api/abstract/cache.py | 4 +- bauh/api/abstract/controller.py | 6 +- bauh/gems/appimage/__init__.py | 8 +- bauh/gems/appimage/config.py | 5 +- bauh/gems/appimage/controller.py | 236 +++++++++++++------------ bauh/gems/appimage/resources/locale/ca | 8 +- bauh/gems/appimage/resources/locale/de | 8 +- bauh/gems/appimage/resources/locale/en | 8 +- bauh/gems/appimage/resources/locale/es | 8 +- bauh/gems/appimage/resources/locale/fr | 8 +- bauh/gems/appimage/resources/locale/it | 8 +- bauh/gems/appimage/resources/locale/pt | 8 +- bauh/gems/appimage/resources/locale/ru | 8 +- bauh/gems/appimage/resources/locale/tr | 8 +- bauh/gems/appimage/worker.py | 177 ++++++++++++------- bauh/view/util/cache.py | 3 +- 18 files changed, 301 insertions(+), 232 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dee08ea9..4595c307 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **git**: for AUR support. ### Improvements +- AppImage + - database updater daemon dropped. Now the database is only downloaded/updated during the initialization process (if it is considered expired). Its expiration are controlled through the new settings property **database.expiration** (in minutes. default: 60 minutes. Use 0 so it is always updated). +

+ +

+ + - old settings properties were dropped (**db_updater.interval**, **db_updated.enabled**) + - database files (**apps.db** and **releases.db**) are now stored at **~/.cache/bauh/appimage** + - displaying a warning when the cached database files could not be found + - Arch - AUR - upgrade checking now considers modification dates as well (needed because not all AUR packages follow versioning standards) diff --git a/README.md b/README.md index e907d30d..fe2da841 100644 --- a/README.md +++ b/README.md @@ -142,18 +142,16 @@ install_channel: false # it allows to select an available channel during the ap - **Upgrade file**: allows to upgrade a manually installed AppImage file - Supported sources: [AppImageHub](https://appimage.github.io) (**applications with no releases published to GitHub are currently not available**) - Installed applications are store at **~/.local/share/bauh/appimage/installed** -- Desktop entries ( menu shortcuts ) of the installed applications are stored at **~/.local/share/applications** +- Desktop entries (menu shortcuts) of the installed applications are stored at **~/.local/share/applications** - Symlinks are created at **~/.local/bin**. They have the same name of the application (if the name already exists, it will be created as 'app_name-appimage'. e.g: 'rpcs3-appimage') -- Downloaded database files are stored at **~/.local/share/bauh/appimage** as **apps.db** and **releases.db** -- Databases are always updated when bauh starts -- Databases updater daemon running every 20 minutes (it can be customized via the configuration file described below) +- Downloaded database files are stored at **~/.cache/bauh/appimage** as **apps.db** and **releases.db** +- Databases are updated during the initialization process if they are considered outdated (after 60 minutes by default, but this behavior can be changed via the AppImage settings) - All supported application names can be found at [apps.txt](https://github.com/vinifmor/bauh-files/blob/master/appimage/apps.txt) - Applications with ignored updates are defined at **~/.config/bauh/appimage/updates_ignored.txt** - The configuration file is located at **~/.config/bauh/appimage.yml** and it allows the following customizations: ``` -db_updater: - enabled: true # if 'false': disables the daemon database updater (bauh will not be able to see if there are updates for your already installed AppImages) - interval: 1200 # the databases update interval in SECONDS (1200 == 20 minutes) +database: + expiration: 60 # defines the period (in minutes) in which the database will be considered up to date during the initialization process. Use 0 if you always want to update it. ``` - Required dependencies - Arch-based systems: **sqlite**, **wget** (or **aria2**/**axel** for faster multi-threaded downloads) diff --git a/bauh/api/abstract/cache.py b/bauh/api/abstract/cache.py index ac3c2534..c0bcabcf 100644 --- a/bauh/api/abstract/cache.py +++ b/bauh/api/abstract/cache.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import Set +from typing import Set, Optional class MemoryCache(ABC): @@ -42,7 +42,7 @@ class MemoryCacheFactory(ABC): """ @abstractmethod - def new(self, expiration: int) -> MemoryCache: + def new(self, expiration: Optional[int]) -> MemoryCache: """ :param expiration: expiration time for the cache keys in seconds. Use -1 to disable this feature. :return: diff --git a/bauh/api/abstract/controller.py b/bauh/api/abstract/controller.py index 335814b9..aaa18d53 100644 --- a/bauh/api/abstract/controller.py +++ b/bauh/api/abstract/controller.py @@ -28,6 +28,10 @@ class SearchResult: self.new = new self.total = total + @classmethod + def empty(cls): + return cls(installed=[], new=[], total=0) + class UpgradeRequirement: @@ -116,7 +120,7 @@ class SoftwareManager(ABC): pass @abstractmethod - def read_installed(self, disk_loader: Optional[DiskCacheLoader], limit: int, only_apps: bool, pkg_types: Set[Type[SoftwarePackage]], internet_available: bool) -> SearchResult: + def read_installed(self, disk_loader: Optional[DiskCacheLoader], limit: int, only_apps: bool, pkg_types: Optional[Set[Type[SoftwarePackage]]], internet_available: bool) -> SearchResult: """ :param disk_loader: a running disk loader thread that loads application data from the disk asynchronously :param limit: the max number of packages to be retrieved. <= 1 should retrieve everything diff --git a/bauh/gems/appimage/__init__.py b/bauh/gems/appimage/__init__.py index b2242bf3..d64ddf62 100644 --- a/bauh/gems/appimage/__init__.py +++ b/bauh/gems/appimage/__init__.py @@ -2,7 +2,7 @@ import os from pathlib import Path from typing import Optional -from bauh.api.constants import CONFIG_PATH +from bauh.api.constants import CONFIG_PATH, CACHE_PATH from bauh.commons import resource ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -13,6 +13,12 @@ CONFIG_FILE = '{}/appimage.yml'.format(CONFIG_PATH) CONFIG_DIR = '{}/appimage'.format(CONFIG_PATH) UPDATES_IGNORED_FILE = '{}/updates_ignored.txt'.format(CONFIG_DIR) SYMLINKS_DIR = '{}/.local/bin'.format(str(Path.home())) +URL_COMPRESSED_DATABASES = 'https://raw.githubusercontent.com/vinifmor/bauh-files/master/appimage/dbs.tar.gz' +DATABASES_DIR = '{}/appimage'.format(CACHE_PATH) +DATABASE_APPS_FILE = '{}/apps.db'.format(DATABASES_DIR) +DATABASE_RELEASES_FILE = '{}/releases.db'.format(DATABASES_DIR) +DATABASES_TS_FILE = '{}/dbs.ts'.format(DATABASES_DIR) +DESKTOP_ENTRIES_PATH = '{}/.local/share/applications'.format(str(Path.home())) def get_icon_path() -> str: diff --git a/bauh/gems/appimage/config.py b/bauh/gems/appimage/config.py index 8ceefa84..6c80328e 100644 --- a/bauh/gems/appimage/config.py +++ b/bauh/gems/appimage/config.py @@ -4,9 +4,8 @@ from bauh.gems.appimage import CONFIG_FILE def read_config(update_file: bool = False) -> dict: default = { - 'db_updater': { - 'interval': 60 * 20, - 'enabled': True + 'database': { + 'expiration': 60 } } return read(CONFIG_FILE, default, update_file=update_file) diff --git a/bauh/gems/appimage/controller.py b/bauh/gems/appimage/controller.py index d55d6571..bbe4241b 100644 --- a/bauh/gems/appimage/controller.py +++ b/bauh/gems/appimage/controller.py @@ -9,7 +9,6 @@ import traceback from datetime import datetime from math import floor from pathlib import Path -from threading import Lock from typing import Set, Type, List, Tuple, Optional from colorama import Fore @@ -29,17 +28,13 @@ from bauh.commons.config import save_config from bauh.commons.html import bold from bauh.commons.system import SystemProcess, new_subprocess, ProcessHandler, run_cmd, SimpleProcess from bauh.gems.appimage import query, INSTALLATION_PATH, LOCAL_PATH, SUGGESTIONS_FILE, CONFIG_FILE, ROOT_DIR, \ - CONFIG_DIR, UPDATES_IGNORED_FILE, util, get_default_manual_installation_file_dir + CONFIG_DIR, UPDATES_IGNORED_FILE, util, get_default_manual_installation_file_dir, DATABASE_APPS_FILE, \ + DATABASE_RELEASES_FILE, DESKTOP_ENTRIES_PATH, DATABASES_DIR from bauh.gems.appimage.config import read_config from bauh.gems.appimage.model import AppImage from bauh.gems.appimage.util import replace_desktop_entry_exec_command from bauh.gems.appimage.worker import DatabaseUpdater, SymlinksVerifier -DB_APPS_PATH = '{}/{}'.format(str(Path.home()), '.local/share/bauh/appimage/apps.db') -DB_RELEASES_PATH = '{}/{}'.format(str(Path.home()), '.local/share/bauh/appimage/releases.db') - -DESKTOP_ENTRIES_PATH = '{}/.local/share/applications'.format(str(Path.home())) - RE_DESKTOP_ICON = re.compile(r'Icon\s*=\s*.+\n') RE_ICON_ENDS_WITH = re.compile(r'.+\.(png|svg)$') RE_APPIMAGE_NAME = re.compile(r'(.+)\.appimage', flags=re.IGNORECASE) @@ -81,7 +76,6 @@ class AppImageManager(SoftwareManager): self.http_client = context.http_client self.logger = context.logger self.file_downloader = context.file_downloader - self.db_locks = {DB_APPS_PATH: Lock(), DB_RELEASES_PATH: Lock()} self.custom_actions = [CustomSoftwareAction(i18n_label_key='appimage.custom_action.install_file', i18n_status_key='appimage.custom_action.install_file.status', manager=self, @@ -177,60 +171,74 @@ class AppImageManager(SoftwareManager): def _get_db_connection(self, db_path: str) -> sqlite3.Connection: if os.path.exists(db_path): - self.db_locks[db_path].acquire() - return sqlite3.connect(db_path) + try: + return sqlite3.connect(db_path) + except: + self.logger.error("Could not connect to database file '{}'".format(db_path)) + traceback.print_exc() else: - self.logger.warning("Could not get a database connection. File '{}' not found".format(db_path)) - - def _close_connection(self, db_path: str, con: sqlite3.Connection): - con.close() - self.db_locks[db_path].release() + self.logger.warning("Could not get a connection for database '{}'".format(db_path)) def _gen_app_key(self, app: AppImage): return '{}{}'.format(app.name.lower(), app.github.lower() if app.github else '') def search(self, words: str, disk_loader: DiskCacheLoader, limit: int = -1, is_url: bool = False) -> SearchResult: if is_url: - return SearchResult([], [], 0) + return SearchResult.empty() - res = SearchResult([], [], 0) - connection = self._get_db_connection(DB_APPS_PATH) + apps_conn = self._get_db_connection(DATABASE_APPS_FILE) - if connection: - try: - cursor = connection.cursor() - cursor.execute(query.SEARCH_APPS_BY_NAME_OR_DESCRIPTION.format(words, words)) + if not apps_conn: + return SearchResult.empty() - found_map = {} - idx = 0 - for r in cursor.fetchall(): - app = AppImage(*r, i18n=self.i18n, custom_actions=self.custom_app_actions) - res.new.append(app) - found_map[self._gen_app_key(app)] = {'app': app, 'idx': idx} - idx += 1 + not_installed, found_map = [], {} - finally: - self._close_connection(DB_APPS_PATH, connection) + try: + cursor = apps_conn.cursor() + cursor.execute(query.SEARCH_APPS_BY_NAME_OR_DESCRIPTION.format(words, words)) - if res.new: - installed = self.read_installed(disk_loader, limit, only_apps=False, pkg_types=None, internet_available=True).installed + idx = 0 + for r in cursor.fetchall(): + app = AppImage(*r, i18n=self.i18n, custom_actions=self.custom_app_actions) + not_installed.append(app) + found_map[self._gen_app_key(app)] = {'app': app, 'idx': idx} + idx += 1 + except: + self.logger.error("An exception happened while querying the 'apps' database") + traceback.print_exc() + apps_conn.close() + return SearchResult.empty() - if installed: - for iapp in installed: - key = self._gen_app_key(iapp) + installed_found = [] - new_found = found_map.get(key) + if not_installed: + installed = self.read_installed(disk_loader=disk_loader, limit=limit, + only_apps=False, + pkg_types=None, + connection=apps_conn, + internet_available=True).installed + if installed: + for appim in installed: + key = self._gen_app_key(appim) - if new_found: - del res.new[new_found['idx']] - res.installed.append(iapp) + new_found = found_map.get(key) - res.total = len(res.installed) + len(res.new) - return res + if new_found: + del not_installed[new_found['idx']] + installed_found.append(appim) - def read_installed(self, disk_loader: DiskCacheLoader, limit: int = -1, only_apps: bool = False, - pkg_types: Set[Type[SoftwarePackage]] = None, internet_available: bool = None, connection: sqlite3.Connection = None) -> SearchResult: - res = SearchResult([], [], 0) + try: + apps_conn.close() + except: + self.logger.error("An exception happened when trying to close the connection to database file '{}'".format(DATABASE_APPS_FILE)) + traceback.print_exc() + + return SearchResult(new=not_installed, installed=installed_found, total=len(not_installed) + len(installed_found)) + + def read_installed(self, disk_loader: Optional[DiskCacheLoader], limit: int = -1, only_apps: bool = False, + pkg_types: Optional[Set[Type[SoftwarePackage]]] = None, internet_available: bool = None, connection: sqlite3.Connection = None) -> SearchResult: + installed_apps = [] + res = SearchResult(installed_apps, [], 0) if os.path.exists(INSTALLATION_PATH): installed = run_cmd('ls {}*/data.json'.format(INSTALLATION_PATH), print_error=False) @@ -243,19 +251,19 @@ class AppImageManager(SoftwareManager): app = AppImage(installed=True, i18n=self.i18n, custom_actions=self.custom_app_actions, **json.loads(f.read())) app.icon_url = app.icon_path - res.installed.append(app) + installed_apps.append(app) names.add("'{}'".format(app.name.lower())) - if res.installed: - con = self._get_db_connection(DB_APPS_PATH) if not connection else connection + if installed_apps: + apps_con = self._get_db_connection(DATABASE_APPS_FILE) if not connection else connection - if con: + if apps_con: try: - cursor = con.cursor() + cursor = apps_con.cursor() cursor.execute(query.FIND_APPS_BY_NAME.format(','.join(names))) for tup in cursor.fetchall(): - for app in res.installed: + for app in installed_apps: if app.name.lower() == tup[0].lower() and (not app.github or app.github.lower() == tup[1].lower()): continuous_version = app.version == 'continuous' continuous_update = tup[2] == 'continuous' @@ -276,15 +284,16 @@ class AppImageManager(SoftwareManager): break except: + self.logger.error("An exception happened while querying the database file {}".format(apps_con)) traceback.print_exc() finally: - if not connection: - self._close_connection(DB_APPS_PATH, con) + if not connection: # the connection can only be closed if it was opened within this method + apps_con.close() ignored_updates = self._read_ignored_updates() if ignored_updates: - for app in res.installed: + for app in installed_apps: if app.supports_ignored_updates() and app.name in ignored_updates: app.updates_ignored = True @@ -406,40 +415,53 @@ class AppImageManager(SoftwareManager): history = [] res = PackageHistory(pkg, history, -1) - connection = self._get_db_connection(DB_APPS_PATH) + app_con = self._get_db_connection(DATABASE_APPS_FILE) - if connection: - try: - cursor = connection.cursor() + if not app_con: + return res - cursor.execute(query.FIND_APP_ID_BY_NAME_AND_GITHUB.format(pkg.name.lower(), pkg.github.lower() if pkg.github else '')) - app_tuple = cursor.fetchone() + try: + cursor = app_con.cursor() - if not app_tuple: - self.logger.warning("Could not retrieve {} from the database {}".format(pkg, DB_APPS_PATH)) - return res - finally: - self._close_connection(DB_APPS_PATH, connection) + cursor.execute(query.FIND_APP_ID_BY_NAME_AND_GITHUB.format(pkg.name.lower(), pkg.github.lower() if pkg.github else '')) + app_tuple = cursor.fetchone() - connection = self._get_db_connection(DB_RELEASES_PATH) + if not app_tuple: + self.logger.warning("Could not retrieve {} from the database {}".format(pkg, DATABASE_APPS_FILE)) + return res + except: + self.logger.error("An exception happened while querying the database file '{}'".format(DATABASE_APPS_FILE)) + traceback.print_exc() + app_con.close() + return res - if connection: - try: - cursor = connection.cursor() + app_con.close() - releases = cursor.execute(query.FIND_RELEASES_BY_APP_ID.format(app_tuple[0])) + releases_con = self._get_db_connection(DATABASE_RELEASES_FILE) - if releases: - for idx, tup in enumerate(releases): - history.append({'0_version': tup[0], '1_published_at': datetime.strptime(tup[2], '%Y-%m-%dT%H:%M:%SZ') if tup[2] else '', '2_url_download': tup[1]}) + if not releases_con: + return res - if res.pkg_status_idx == -1 and pkg.version == tup[0]: - res.pkg_status_idx = idx + try: + cursor = releases_con.cursor() - finally: - self._close_connection(DB_RELEASES_PATH, connection) + releases = cursor.execute(query.FIND_RELEASES_BY_APP_ID.format(app_tuple[0])) - return res + if releases: + for idx, tup in enumerate(releases): + history.append({'0_version': tup[0], + '1_published_at': datetime.strptime(tup[2], '%Y-%m-%dT%H:%M:%SZ') if tup[ + 2] else '', '2_url_download': tup[1]}) + + if res.pkg_status_idx == -1 and pkg.version == tup[0]: + res.pkg_status_idx = idx + + return res + except: + self.logger.error("An exception happened while querying the database file '{}'".format(DATABASE_RELEASES_FILE)) + traceback.print_exc() + finally: + releases_con.close() def _find_desktop_file(self, folder: str) -> str: for r, d, files in os.walk(folder): @@ -452,7 +474,7 @@ class AppImageManager(SoftwareManager): if RE_ICON_ENDS_WITH.match(f): return f - def install(self, pkg: AppImage, root_password: str, disk_loader: DiskCacheLoader, watcher: ProcessWatcher) -> TransactionResult: + def install(self, pkg: AppImage, root_password: str, disk_loader: Optional[DiskCacheLoader], watcher: ProcessWatcher) -> TransactionResult: handler = ProcessHandler(watcher) out_dir = INSTALLATION_PATH + pkg.get_clean_name() @@ -587,22 +609,17 @@ class AppImageManager(SoftwareManager): return False def prepare(self, task_manager: TaskManager, root_password: str, internet_available: bool): - local_config = read_config(update_file=True) - interval = local_config['db_updater']['interval'] or 20 * 60 - - updater = DatabaseUpdater(task_man=task_manager, - i18n=self.context.i18n, - http_client=self.context.http_client, logger=self.context.logger, - db_locks=self.db_locks, interval=interval, - internet_checker=self.context.internet_checker) - if local_config['db_updater']['enabled']: - updater.start() - elif internet_available: - updater.download_databases() # only once - symlink_check = SymlinksVerifier(taskman=task_manager, i18n=self.i18n, logger=self.logger) symlink_check.start() + if internet_available: + updater = DatabaseUpdater(task_man=task_manager, i18n=self.context.i18n, + http_client=self.context.http_client, logger=self.context.logger) + + if updater.should_update(read_config()): + updater.register_task() + updater.start() + def list_updates(self, internet_available: bool) -> List[PackageUpdate]: res = self.read_installed(disk_loader=None, internet_available=internet_available) @@ -615,12 +632,15 @@ class AppImageManager(SoftwareManager): return updates def list_warnings(self, internet_available: bool) -> List[str]: - pass + dbfiles = glob.glob('{}/*.db'.format(DATABASES_DIR)) + + if not dbfiles or len({f for f in (DATABASE_APPS_FILE, DATABASE_RELEASES_FILE) if f in dbfiles}) != 2: + return [self.i18n['appimage.warning.missing_db_files'].format(appimage=bold('AppImage'))] def list_suggestions(self, limit: int, filter_installed: bool) -> List[PackageSuggestion]: res = [] - connection = self._get_db_connection(DB_APPS_PATH) + connection = self._get_db_connection(DATABASE_APPS_FILE) if connection: file = self.http_client.get(SUGGESTIONS_FILE) @@ -661,7 +681,7 @@ class AppImageManager(SoftwareManager): except: traceback.print_exc() finally: - self._close_connection(DB_APPS_PATH, connection) + connection.close() return res @@ -705,34 +725,22 @@ class AppImageManager(SoftwareManager): config = read_config() max_width = floor(screen_width * 0.15) - enabled_opts = [InputOption(label=self.i18n['yes'].capitalize(), value=True), - InputOption(label=self.i18n['no'].capitalize(), value=False)] - - updater_opts = [ - SingleSelectComponent(label=self.i18n['appimage.config.db_updates.activated'], - options=enabled_opts, - default_option=[o for o in enabled_opts if o.value == config['db_updater']['enabled']][0], - max_per_line=len(enabled_opts), - type_=SelectViewType.RADIO, - tooltip=self.i18n['appimage.config.db_updates.activated.tip'], - max_width=max_width, - id_='up_enabled'), - TextInputComponent(label=self.i18n['interval'], - value=str(config['db_updater']['interval']), - tooltip=self.i18n['appimage.config.db_updates.interval.tip'], + opts = [ + TextInputComponent(label=self.i18n['appimage.config.database.expiration'], + value=int(config['database']['expiration']) if isinstance(config['database']['expiration'], int) else '', + tooltip=self.i18n['appimage.config.database.expiration.tip'], only_int=True, max_width=max_width, - id_='up_int') + id_='appim_db_exp') ] - return PanelComponent([FormComponent(updater_opts, self.i18n['appimage.config.db_updates'])]) + return PanelComponent([FormComponent(opts, self.i18n['appimage.config.database'])]) def save_settings(self, component: PanelComponent) -> Tuple[bool, Optional[List[str]]]: config = read_config() panel = component.components[0] - config['db_updater']['enabled'] = panel.get_component('up_enabled').get_selected() - config['db_updater']['interval'] = panel.get_component('up_int').get_int_value() + config['database']['expiration'] = panel.get_component('appim_db_exp').get_int_value() try: save_config(config, CONFIG_FILE) diff --git a/bauh/gems/appimage/resources/locale/ca b/bauh/gems/appimage/resources/locale/ca index bfb71078..37147d06 100644 --- a/bauh/gems/appimage/resources/locale/ca +++ b/bauh/gems/appimage/resources/locale/ca @@ -1,7 +1,6 @@ -appimage.config.db_updates=Database update -appimage.config.db_updates.activated=activated -appimage.config.db_updates.activated.tip=It will be possible to check for updates related to the installed applications -appimage.config.db_updates.interval.tip=Update interval in SECONDS +appimage.config.database=Database +appimage.config.database.expiration=Expiration +appimage.config.database.expiration.tip=It defines the period (in minutes) in which the database will be considered up to date during the initialization process. Use 0 if you always want to update it. appimage.custom_action.install_file=Install AppImage file appimage.custom_action.install_file.details=Installation details appimage.custom_action.install_file.invalid_file=You must define a valid AppImage file @@ -34,5 +33,6 @@ appimage.install.permission=S’està concedint el permís d’execució a {} appimage.task.db_update=Actualització de bases de dades appimage.task.symlink_check=Checking symlinks appimage.uninstall.error.remove_folder=No s’ha pogut suprimir el directori d’instal·lació de l’aplicació {} +appimage.warning.missing_db_files={appimage} database files were not found. Some actions will not work properly. gem.appimage.info=Aplicacions publicades a https://appimage.github.io/ gem.appimage.label=AppImage \ No newline at end of file diff --git a/bauh/gems/appimage/resources/locale/de b/bauh/gems/appimage/resources/locale/de index e7a36881..fc365c97 100644 --- a/bauh/gems/appimage/resources/locale/de +++ b/bauh/gems/appimage/resources/locale/de @@ -1,7 +1,6 @@ -appimage.config.db_updates=Database update -appimage.config.db_updates.activated=activated -appimage.config.db_updates.activated.tip=It will be possible to check for updates related to the installed applications -appimage.config.db_updates.interval.tip=Update interval in SECONDS +appimage.config.database=Database +appimage.config.database.expiration=Expiration +appimage.config.database.expiration.tip=It defines the period (in minutes) in which the database will be considered up to date during the initialization process. Use 0 if you always want to update it. appimage.custom_action.install_file=Install AppImage file appimage.custom_action.install_file.details=Installation details appimage.custom_action.install_file.invalid_file=You must define a valid AppImage file @@ -34,5 +33,6 @@ appimage.install.permission=Ausführberechtigungen für {} appimage.task.db_update=Updating databases appimage.task.symlink_check=Checking symlinks appimage.uninstall.error.remove_folder=Anwendungordner {} konnte nicht entfernt werden +appimage.warning.missing_db_files={appimage} database files were not found. Some actions will not work properly. gem.appimage.info=Anwendungen veröffentlicht auf https://appimage.github.io/ gem.appimage.label=AppImage \ No newline at end of file diff --git a/bauh/gems/appimage/resources/locale/en b/bauh/gems/appimage/resources/locale/en index d69efe5a..142f777a 100644 --- a/bauh/gems/appimage/resources/locale/en +++ b/bauh/gems/appimage/resources/locale/en @@ -1,7 +1,6 @@ -appimage.config.db_updates=Database update -appimage.config.db_updates.activated=activated -appimage.config.db_updates.activated.tip=It will be possible to check for updates related to the installed applications -appimage.config.db_updates.interval.tip=Update interval in SECONDS +appimage.config.database=Database +appimage.config.database.expiration=Expiration +appimage.config.database.expiration.tip=appimage.config.database.expiration.tip=It defines the period (in minutes) in which the database will be considered up to date. Use 0 if you always want to update it. appimage.custom_action.install_file=Install AppImage file appimage.custom_action.install_file.details=Installation details appimage.custom_action.install_file.invalid_file=You must define a valid AppImage file @@ -34,5 +33,6 @@ appimage.install.permission=Giving execution permission to {} appimage.task.db_update=Updating databases appimage.task.symlink_check=Checking symlinks appimage.uninstall.error.remove_folder=Could not remove the application installation directory {} +appimage.warning.missing_db_files={appimage} database files were not found. Some actions will not work properly. gem.appimage.info=Applications published at https://appimage.github.io/ gem.appimage.label=AppImage \ No newline at end of file diff --git a/bauh/gems/appimage/resources/locale/es b/bauh/gems/appimage/resources/locale/es index 47adc1b8..a7b88024 100644 --- a/bauh/gems/appimage/resources/locale/es +++ b/bauh/gems/appimage/resources/locale/es @@ -1,7 +1,6 @@ -appimage.config.db_updates=Actualización de la base de datos -appimage.config.db_updates.activated=activada -appimage.config.db_updates.activated.tip=Será posible buscar actualizaciones relacionadas con las aplicaciones instaladas -appimage.config.db_updates.interval.tip=Intervalo de actualización en SEGUNDOS +appimage.config.database=Base de datos +appimage.config.database.expiration=Expiración +appimage.config.database.expiration.tip=Define el período (en minutos) en el que la base de datos será considerada actualizada durante el proceso de inicialización. Use 0 si siempre desea actualizarla. appimage.custom_action.install_file=Instalar archivo AppImage appimage.custom_action.install_file.details=Detalles de instalación appimage.custom_action.install_file.invalid_file=Debe definir un archivo AppImage válido @@ -34,5 +33,6 @@ appimage.install.permission=Concediendo permiso de ejecución a {} appimage.task.db_update=Actualizando base de datos appimage.task.symlink_check=Verificando links simbólicos appimage.uninstall.error.remove_folder=No se pudo eliminar el directorio de instalación de la aplicación {} +appimage.warning.missing_db_files=No se encontraron archivos de la base de datos {appimage}. Algunas acciones no funcionarán correctamente. gem.appimage.info=Aplicativos publicados en https://appimage.github.io/ gem.appimage.label=AppImage \ No newline at end of file diff --git a/bauh/gems/appimage/resources/locale/fr b/bauh/gems/appimage/resources/locale/fr index e263fb0c..484322d9 100644 --- a/bauh/gems/appimage/resources/locale/fr +++ b/bauh/gems/appimage/resources/locale/fr @@ -1,7 +1,6 @@ -appimage.config.db_updates=Mise à jour de la base de données -appimage.config.db_updates.activated=activé -appimage.config.db_updates.activated.tip=La recherche de mises à jour liées aux applications installées sera possible -appimage.config.db_updates.interval.tip=Intervalle de mises à jour en SECONDES +appimage.config.database=Database +appimage.config.database.expiration=Expiration +appimage.config.database.expiration.tip=It defines the period (in minutes) in which the database will be considered up to date. Use 0 if you always want to update it. appimage.custom_action.install_file=Installer fichier AppImage appimage.custom_action.install_file.details=Détails d'installation appimage.custom_action.install_file.invalid_file=Vous devez définir un fichier AppImage valide @@ -34,5 +33,6 @@ appimage.install.permission={} est maintenant exécutable appimage.task.db_update=Mise à jour des bases de données appimage.task.symlink_check=Verification des symlinks appimage.uninstall.error.remove_folder=Impossible de supprimer le répertoire d'installation {} +appimage.warning.missing_db_files={appimage} database files were not found. Some actions will not work properly. gem.appimage.info=Applications publiées à https://appimage.github.io/ gem.appimage.label=AppImage diff --git a/bauh/gems/appimage/resources/locale/it b/bauh/gems/appimage/resources/locale/it index df393fef..76bf6a6c 100644 --- a/bauh/gems/appimage/resources/locale/it +++ b/bauh/gems/appimage/resources/locale/it @@ -1,7 +1,6 @@ -appimage.config.db_updates=Database update -appimage.config.db_updates.activated=activated -appimage.config.db_updates.activated.tip=It will be possible to check for updates related to the installed applications -appimage.config.db_updates.interval.tip=Update interval in SECONDS +appimage.config.database=Database +appimage.config.database.expiration=Expiration +appimage.config.database.expiration.tip=It defines the period (in minutes) in which the database will be considered up to date during the initialization process. Use 0 if you always want to update it. appimage.custom_action.install_file=Install AppImage file appimage.custom_action.install_file.details=Installation details appimage.custom_action.install_file.invalid_file=You must define a valid AppImage file @@ -34,5 +33,6 @@ appimage.install.permission=Gdare il permesso di esecuzione a {} appimage.task.db_update=Aggiornamento dei database appimage.task.symlink_check=Checking symlinks appimage.uninstall.error.remove_folder=Impossibile rimuovere la directory di installazione dell'applicazione {} +appimage.warning.missing_db_files={appimage} database files were not found. Some actions will not work properly. gem.appimage.info=Applicazioni pubblicate su https://appimage.github.io/ gem.appimage.label=AppImage \ No newline at end of file diff --git a/bauh/gems/appimage/resources/locale/pt b/bauh/gems/appimage/resources/locale/pt index be125a45..27dfbe43 100644 --- a/bauh/gems/appimage/resources/locale/pt +++ b/bauh/gems/appimage/resources/locale/pt @@ -1,7 +1,6 @@ -appimage.config.db_updates=Atualização da base de dados -appimage.config.db_updates.activated=ativada -appimage.config.db_updates.activated.tip=Será possível verificar se há atualizações para os aplicativos instalados -appimage.config.db_updates.interval.tip=Intervalo de atualização em SEGUNDOS +appimage.config.database=Banco de dados +appimage.config.database.expiration=Expiração +appimage.config.database.expiration.tip=Define o período (em minutos) no qual o banco de dados será considerado atualizado durante o processo de inicialização. Use 0 se você quiser que ele sempre seja atualizado. appimage.custom_action.install_file=Instalar arquivo AppImage appimage.custom_action.install_file.details=Detalhes de instalação appimage.custom_action.install_file.invalid_file=Você precisa definir um arquivo AppImage válido @@ -34,5 +33,6 @@ appimage.install.permission=Concedendo permissão de execução para {} appimage.task.db_update=Atualizando base de dados appimage.task.symlink_check=Verificando links simbólicos appimage.uninstall.error.remove_folder=Não foi possível remover o diretóri ode instalação do aplicativo {} +appimage.warning.missing_db_files=Arquivos do banco de dados {appimage} não foram encontrados. Algumas ações não funcionarão corretamente. gem.appimage.info=Aplicativos publicados em https://appimage.github.io/ gem.appimage.label=AppImage \ No newline at end of file diff --git a/bauh/gems/appimage/resources/locale/ru b/bauh/gems/appimage/resources/locale/ru index eaaac1f5..2a072713 100644 --- a/bauh/gems/appimage/resources/locale/ru +++ b/bauh/gems/appimage/resources/locale/ru @@ -1,7 +1,6 @@ -appimage.config.db_updates=Обновление базы данных -appimage.config.db_updates.activated=Активировать -appimage.config.db_updates.activated.tip=Позволит проверять обновления установленных приложений -appimage.config.db_updates.interval.tip=Интервал обновления в секундах +appimage.config.database=Database +appimage.config.database.expiration=Expiration +appimage.config.database.expiration.tip=It defines the period (in minutes) in which the database will be considered up to date during the initialization process. Use 0 if you always want to update it. appimage.custom_action.install_file=Установить файл AppImage appimage.custom_action.install_file.details=Детали установки appimage.custom_action.install_file.invalid_file=Вы должны определить допустимый файл AppImage @@ -34,5 +33,6 @@ appimage.install.permission=Установить права на выполне appimage.task.db_update=Обновление базы данных appimage.task.symlink_check=Checking symlinks appimage.uninstall.error.remove_folder=Не удалось удалить каталог приложения {} +appimage.warning.missing_db_files={appimage} database files were not found. Some actions will not work properly. gem.appimage.info=Приложения, опубликованные на https://appimage.github.io/ gem.appimage.label=AppImage diff --git a/bauh/gems/appimage/resources/locale/tr b/bauh/gems/appimage/resources/locale/tr index 6dd8e0cb..232ff11e 100644 --- a/bauh/gems/appimage/resources/locale/tr +++ b/bauh/gems/appimage/resources/locale/tr @@ -1,7 +1,6 @@ -appimage.config.db_updates=Veritabanı güncelle -appimage.config.db_updates.activated=Aktifleştirildi -appimage.config.db_updates.activated.tip=Yüklü uygulamalarla ilgili güncellemeleri kontrol etmek mümkün olacak -appimage.config.db_updates.interval.tip=SANİYE cinsinden güncelleme aralığı +appimage.config.database=Database +appimage.config.database.expiration=Expiration +appimage.config.database.expiration.tip=It defines the period (in minutes) in which the database will be considered up to date during the initialization process. Use 0 if you always want to update it. appimage.custom_action.install_file=AppImage dosyasını yükle appimage.custom_action.install_file.details=Kurulum detayları appimage.custom_action.install_file.invalid_file=Geçerli bir AppImage dosyası tanımlamanız gerekir @@ -34,5 +33,6 @@ appimage.install.permission={} için yürütme izni veriliyor appimage.task.db_update=Veritabanları güncelleniyor appimage.task.symlink_check=Checking symlinks appimage.uninstall.error.remove_folder={} uygulama kurulum dizini kaldırılamadı +appimage.warning.missing_db_files={appimage} database files were not found. Some actions will not work properly. gem.appimage.info=https://appimage.github.io/ adresinde yayınlanan uygulamalar gem.appimage.label=AppImage diff --git a/bauh/gems/appimage/worker.py b/bauh/gems/appimage/worker.py index ab3673e2..678942c7 100644 --- a/bauh/gems/appimage/worker.py +++ b/bauh/gems/appimage/worker.py @@ -5,108 +5,151 @@ import os import tarfile import time import traceback +from datetime import datetime, timedelta from pathlib import Path from threading import Thread - -import requests +from typing import Optional from bauh.api.abstract.handler import TaskManager, ProcessWatcher from bauh.api.http import HttpClient -from bauh.commons.internet import InternetChecker -from bauh.gems.appimage import LOCAL_PATH, get_icon_path, INSTALLATION_PATH, SYMLINKS_DIR, util +from bauh.gems.appimage import get_icon_path, INSTALLATION_PATH, SYMLINKS_DIR, util, DATABASES_TS_FILE, \ + DATABASES_DIR, DATABASE_APPS_FILE, DATABASE_RELEASES_FILE, URL_COMPRESSED_DATABASES from bauh.gems.appimage.model import AppImage from bauh.view.util.translation import I18n class DatabaseUpdater(Thread): - URL_DB = 'https://raw.githubusercontent.com/vinifmor/bauh-files/master/appimage/dbs.tar.gz' - COMPRESS_FILE_PATH = LOCAL_PATH + '/db.tar.gz' + COMPRESS_FILE_PATH = '{}/db.tar.gz'.format(DATABASES_DIR) - def __init__(self, task_man: TaskManager, i18n: I18n, http_client: HttpClient, - logger: logging.Logger, db_locks: dict, interval: int, - internet_checker: InternetChecker): + def __init__(self, task_man: TaskManager, i18n: I18n, http_client: HttpClient, logger: logging.Logger): super(DatabaseUpdater, self).__init__(daemon=True) self.http_client = http_client self.logger = logger - self.db_locks = db_locks - self.sleep_time = interval self.i18n = i18n self.task_man = task_man self.task_id = 'appim_db' - self.internet_checker = internet_checker + + def should_update(self, appimage_config: dict) -> bool: + ti = time.time() + + try: + db_exp = int(appimage_config['database']['expiration']) + except ValueError: + self.logger.error("Could not parse settings property 'database.expiration': {}".format(appimage_config['database']['expiration'])) + return True + + if db_exp <= 0: + self.logger.info("No expiration time configured for the AppImage database") + return True + + files = {*glob.glob('{}/*'.format(DATABASES_DIR))} + + if not files: + self.logger.warning('No database files on {}'.format(DATABASES_DIR)) + return True + + if DATABASES_TS_FILE not in files: + self.logger.warning("No database timestamp file found ({})".format(DATABASES_TS_FILE)) + return True + + if DATABASE_APPS_FILE not in files: + self.logger.warning("Database file '{}' not found".format(DATABASE_APPS_FILE)) + return True + + if DATABASE_RELEASES_FILE not in files: + self.logger.warning("Database file '{}' not found".format(DATABASE_RELEASES_FILE)) + return True + + with open(DATABASES_TS_FILE) as f: + dbs_ts_str = f.read() + + try: + dbs_timestamp = datetime.fromtimestamp(float(dbs_ts_str)) + except: + self.logger.error('Could not parse the databases timestamp: {}'.format(dbs_ts_str)) + traceback.print_exc() + return True + + update = dbs_timestamp + timedelta(minutes=db_exp) <= datetime.utcnow() + self.logger.info('Finished. Took {0:.2f} seconds'.format(time.time() - ti)) + return update + + def register_task(self): + if self.task_man: + self.task_man.register_task(self.task_id, self.i18n['appimage.task.db_update'], get_icon_path()) def _finish_task(self): if self.task_man: self.task_man.update_progress(self.task_id, 100, None) self.task_man.finish_task(self.task_id) self.task_man = None + self.logger.info("Finished") + + def _update_task_progress(self, progress: float, substatus: Optional[str] = None): + if self.task_man: + self.task_man.update_progress(self.task_id, progress, substatus) def download_databases(self): - if self.task_man: - self.task_man.register_task(self.task_id, self.i18n['appimage.task.db_update'], get_icon_path()) - self.task_man.update_progress(self.task_id, 10, None) - - try: - if not self.internet_checker.is_available(): - self._finish_task() - return - except requests.exceptions.ConnectionError: - self.logger.warning('The internet connection seems to be off.') - self._finish_task() - return - + self._update_task_progress(1) # TODO add substatus self.logger.info('Retrieving AppImage databases') + database_timestamp = datetime.utcnow().timestamp() try: - res = self.http_client.get(self.URL_DB, session=False) + res = self.http_client.get(URL_COMPRESSED_DATABASES, session=False) except Exception as e: self.logger.error("An error ocurred while downloading the AppImage database: {}".format(e.__class__.__name__)) res = None - if res: - Path(LOCAL_PATH).mkdir(parents=True, exist_ok=True) - - with open(self.COMPRESS_FILE_PATH, 'wb+') as f: - f.write(res.content) - - self.logger.info("Database file saved at {}".format(self.COMPRESS_FILE_PATH)) - - old_db_files = glob.glob(LOCAL_PATH + '/*.db') - - if old_db_files: - self.logger.info('Deleting old database files') - for f in old_db_files: - self.db_locks[f].acquire() - try: - os.remove(f) - finally: - self.db_locks[f].release() - - self.logger.info('Old database files deleted') - - self.logger.info('Uncompressing {}'.format(self.COMPRESS_FILE_PATH)) - - try: - tf = tarfile.open(self.COMPRESS_FILE_PATH) - tf.extractall(LOCAL_PATH) - self.logger.info('Successfully uncompressed file {}'.format(self.COMPRESS_FILE_PATH)) - except: - self.logger.error('Could not extract file {}'.format(self.COMPRESS_FILE_PATH)) - traceback.print_exc() - finally: - self.logger.info('Deleting {}'.format(self.COMPRESS_FILE_PATH)) - os.remove(self.COMPRESS_FILE_PATH) - self.logger.info('Successfully removed {}'.format(self.COMPRESS_FILE_PATH)) - self._finish_task() - else: - self.logger.warning('Could not download the database file {}'.format(self.URL_DB)) + if not res: + self.logger.warning('Could not download the database file {}'.format(URL_COMPRESSED_DATABASES)) self._finish_task() + return + + self._update_task_progress(25) # TODO add substatus + Path(DATABASES_DIR).mkdir(parents=True, exist_ok=True) + + with open(self.COMPRESS_FILE_PATH, 'wb+') as f: + f.write(res.content) + + self.logger.info("Database file saved at {}".format(self.COMPRESS_FILE_PATH)) + + self._update_task_progress(50) # TODO add substatus + old_db_files = glob.glob(DATABASES_DIR + '/*.db') + + if old_db_files: + self.logger.info('Deleting old database files') + for f in old_db_files: + os.remove(f) + + self.logger.info('Old database files deleted') + + self._update_task_progress(75) # TODO add substatus + self.logger.info('Uncompressing {}'.format(self.COMPRESS_FILE_PATH)) + + try: + tf = tarfile.open(self.COMPRESS_FILE_PATH) + tf.extractall(DATABASES_DIR) + self.logger.info('Successfully uncompressed file {}'.format(self.COMPRESS_FILE_PATH)) + except: + self.logger.error('Could not extract file {}'.format(self.COMPRESS_FILE_PATH)) + traceback.print_exc() + finally: + self.logger.info('Deleting {}'.format(self.COMPRESS_FILE_PATH)) + os.remove(self.COMPRESS_FILE_PATH) + self.logger.info('File {} deleted'.format(self.COMPRESS_FILE_PATH)) + + self._update_task_progress(95) + self.logger.info("Saving database timestamp {}".format(database_timestamp)) + + with open(DATABASES_TS_FILE, 'w+') as f: + f.write(str(database_timestamp)) + + self.logger.info("Database timestamp saved") + + self._finish_task() def run(self): - while True: - self.download_databases() - self.logger.info('Sleeping') - time.sleep(self.sleep_time) + self.download_databases() class SymlinksVerifier(Thread): diff --git a/bauh/view/util/cache.py b/bauh/view/util/cache.py index b4ac1533..963edd0d 100644 --- a/bauh/view/util/cache.py +++ b/bauh/view/util/cache.py @@ -1,6 +1,7 @@ import datetime import time from threading import Lock, Thread +from typing import Optional from bauh.api.abstract.cache import MemoryCache, MemoryCacheFactory @@ -106,7 +107,7 @@ class DefaultMemoryCacheFactory(MemoryCacheFactory): self.expiration_time = expiration_time self.cleaner = cleaner - def new(self, expiration: int = None) -> MemoryCache: + def new(self, expiration: Optional[int] = None) -> MemoryCache: instance = DefaultMemoryCache(expiration if expiration is not None else self.expiration_time) if self.cleaner: