mirror of
https://github.com/spalencsar/bearhub.git
synced 2026-07-06 22:54:16 +02:00
feature: custom software suggestions
This commit is contained in:
@@ -9,7 +9,6 @@ from bauh.commons import resource
|
||||
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
APPIMAGE_SHARED_DIR = f'{SHARED_FILES_DIR}/appimage'
|
||||
INSTALLATION_DIR = f'{APPIMAGE_SHARED_DIR}/installed'
|
||||
SUGGESTIONS_FILE = f'https://raw.githubusercontent.com/vinifmor/{__app_name__}-files/master/appimage/suggestions.txt'
|
||||
CONFIG_FILE = f'{CONFIG_DIR}/appimage.yml'
|
||||
APPIMAGE_CONFIG_DIR = f'{CONFIG_DIR}/appimage'
|
||||
UPDATES_IGNORED_FILE = f'{APPIMAGE_CONFIG_DIR}/updates_ignored.txt'
|
||||
@@ -19,8 +18,6 @@ APPIMAGE_CACHE_DIR = f'{CACHE_DIR}/appimage'
|
||||
DATABASE_APPS_FILE = f'{APPIMAGE_CACHE_DIR}/apps.db'
|
||||
DATABASE_RELEASES_FILE = f'{APPIMAGE_CACHE_DIR}/releases.db'
|
||||
DATABASES_TS_FILE = f'{APPIMAGE_CACHE_DIR}/dbs.ts'
|
||||
SUGGESTIONS_CACHED_FILE = f'{APPIMAGE_CACHE_DIR}/suggestions.txt'
|
||||
SUGGESTIONS_CACHED_TS_FILE = f'{APPIMAGE_CACHE_DIR}/suggestions.ts'
|
||||
DOWNLOAD_DIR = f'{TEMP_DIR}/appimage/download'
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import sqlite3
|
||||
import subprocess
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from math import floor
|
||||
from pathlib import Path
|
||||
from typing import Set, Type, List, Tuple, Optional, Iterable, Generator
|
||||
|
||||
@@ -83,6 +82,7 @@ class AppImageManager(SoftwareManager, SettingsController):
|
||||
self._action_self_install: Optional[CustomSoftwareAction] = None
|
||||
self._app_github: Optional[str] = None
|
||||
self._search_unfilled_attrs: Optional[Tuple[str, ...]] = None
|
||||
self._suggestions_downloader: Optional[AppImageSuggestionsDownloader] = None
|
||||
|
||||
def install_file(self, root_password: Optional[str], watcher: ProcessWatcher) -> bool:
|
||||
max_width = 350
|
||||
@@ -746,9 +746,11 @@ class AppImageManager(SoftwareManager, SettingsController):
|
||||
create_config=create_config, http_client=self.context.http_client,
|
||||
logger=self.context.logger).start()
|
||||
|
||||
AppImageSuggestionsDownloader(taskman=task_manager, i18n=self.context.i18n,
|
||||
http_client=self.context.http_client, logger=self.context.logger,
|
||||
create_config=create_config).start()
|
||||
if not self.suggestions_downloader.is_custom_local_file_mapped():
|
||||
self.suggestions_downloader.taskman = task_manager
|
||||
self.suggestions_downloader.create_config = create_config
|
||||
self.suggestions_downloader.register_task()
|
||||
self.suggestions_downloader.start()
|
||||
|
||||
def list_updates(self, internet_available: bool) -> List[PackageUpdate]:
|
||||
res = self.read_installed(disk_loader=None, internet_available=internet_available)
|
||||
@@ -768,23 +770,24 @@ class AppImageManager(SoftwareManager, SettingsController):
|
||||
return [self.i18n['appimage.warning.missing_db_files'].format(appimage=bold('AppImage'))]
|
||||
|
||||
def list_suggestions(self, limit: int, filter_installed: bool) -> Optional[List[PackageSuggestion]]:
|
||||
res = []
|
||||
if limit == 0:
|
||||
return
|
||||
|
||||
connection = self._get_db_connection(DATABASE_APPS_FILE)
|
||||
|
||||
if connection:
|
||||
suggestions = AppImageSuggestionsDownloader(appimage_config=self.configman.get_config(), logger=self.logger,
|
||||
i18n=self.i18n, http_client=self.http_client,
|
||||
taskman=TaskManager()).read()
|
||||
self.suggestions_downloader.taskman = TaskManager()
|
||||
suggestions = tuple(self.suggestions_downloader.read())
|
||||
|
||||
if not suggestions:
|
||||
self.logger.warning("Could not read suggestions")
|
||||
return res
|
||||
self.logger.warning("Could not read AppImage suggestions")
|
||||
return
|
||||
else:
|
||||
self.logger.info("Mapping suggestions")
|
||||
self.logger.info("Mapping AppImage suggestions")
|
||||
try:
|
||||
if filter_installed:
|
||||
installed = {i.name.lower() for i in self.read_installed(disk_loader=None, connection=connection).installed}
|
||||
installed = {i.name.lower() for i in self.read_installed(disk_loader=None,
|
||||
connection=connection).installed}
|
||||
else:
|
||||
installed = None
|
||||
|
||||
@@ -795,7 +798,7 @@ class AppImageManager(SoftwareManager, SettingsController):
|
||||
|
||||
name = lsplit[1].strip()
|
||||
|
||||
if limit <= 0 or len(sugs_map) < limit:
|
||||
if limit < 0 or len(sugs_map) < limit:
|
||||
if not installed or not name.lower() in installed:
|
||||
sugs_map[name] = SuggestionPriority(int(lsplit[0]))
|
||||
else:
|
||||
@@ -804,17 +807,18 @@ class AppImageManager(SoftwareManager, SettingsController):
|
||||
cursor = connection.cursor()
|
||||
cursor.execute(query.FIND_APPS_BY_NAME_FULL.format(','.join([f"'{s}'" for s in sugs_map.keys()])))
|
||||
|
||||
res = []
|
||||
for t in cursor.fetchall():
|
||||
app = AppImage(*t, i18n=self.i18n)
|
||||
res.append(PackageSuggestion(app, sugs_map[app.name.lower()]))
|
||||
self.logger.info(f"Mapped {len(res)} suggestions")
|
||||
|
||||
self.logger.info(f"Mapped {len(res)} AppImage suggestions")
|
||||
return res
|
||||
except:
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
return res
|
||||
|
||||
def is_default_enabled(self) -> bool:
|
||||
return True
|
||||
|
||||
@@ -1080,3 +1084,18 @@ class AppImageManager(SoftwareManager, SettingsController):
|
||||
self._app_github = f'vinifmor/{self.context.app_name}'
|
||||
|
||||
return self._app_github
|
||||
|
||||
@property
|
||||
def suggestions_downloader(self) -> AppImageSuggestionsDownloader:
|
||||
if not self._suggestions_downloader:
|
||||
file_url = self.context.get_suggestion_url(self.__module__)
|
||||
self._suggestions_downloader = AppImageSuggestionsDownloader(taskman=TaskManager(),
|
||||
i18n=self.context.i18n,
|
||||
http_client=self.context.http_client,
|
||||
logger=self.context.logger,
|
||||
file_url=file_url)
|
||||
|
||||
if self._suggestions_downloader.is_custom_local_file_mapped():
|
||||
self.logger.info(f"Local AppImage suggestions file mapped: {file_url}")
|
||||
|
||||
return self._suggestions_downloader
|
||||
|
||||
@@ -8,7 +8,7 @@ import traceback
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from threading import Thread
|
||||
from typing import Optional, List
|
||||
from typing import Optional, Generator
|
||||
|
||||
import requests
|
||||
|
||||
@@ -17,8 +17,7 @@ from bauh.api.http import HttpClient
|
||||
from bauh.commons.boot import CreateConfigFile
|
||||
from bauh.commons.html import bold
|
||||
from bauh.gems.appimage import get_icon_path, INSTALLATION_DIR, SYMLINKS_DIR, util, DATABASES_TS_FILE, \
|
||||
APPIMAGE_CACHE_DIR, DATABASE_APPS_FILE, DATABASE_RELEASES_FILE, URL_COMPRESSED_DATABASES, SUGGESTIONS_FILE, \
|
||||
SUGGESTIONS_CACHED_TS_FILE, SUGGESTIONS_CACHED_FILE
|
||||
APPIMAGE_CACHE_DIR, DATABASE_APPS_FILE, DATABASE_RELEASES_FILE, URL_COMPRESSED_DATABASES
|
||||
from bauh.gems.appimage.model import AppImage
|
||||
from bauh.view.util.translation import I18n
|
||||
|
||||
@@ -286,7 +285,9 @@ class SymlinksVerifier(Thread):
|
||||
|
||||
class AppImageSuggestionsDownloader(Thread):
|
||||
|
||||
def __init__(self, logger: logging.Logger, http_client: HttpClient, i18n: I18n, taskman: TaskManager, create_config: Optional[CreateConfigFile] = None, appimage_config: Optional[dict] = None):
|
||||
def __init__(self, logger: logging.Logger, http_client: HttpClient, i18n: I18n, file_url: Optional[str],
|
||||
create_config: Optional[CreateConfigFile] = None, appimage_config: Optional[dict] = None,
|
||||
taskman: Optional[TaskManager] = None):
|
||||
super(AppImageSuggestionsDownloader, self).__init__(daemon=True)
|
||||
self.create_config = create_config
|
||||
self.logger = logger
|
||||
@@ -295,133 +296,189 @@ class AppImageSuggestionsDownloader(Thread):
|
||||
self.taskman = taskman
|
||||
self.config = appimage_config
|
||||
self.task_id = 'appim.suggestions'
|
||||
self.taskman.register_task(id_=self.task_id, label=i18n['task.download_suggestions'], icon_path=get_icon_path())
|
||||
self._cached_file_path: Optional[str] = None
|
||||
self._cached_ts_file_path: Optional[str] = None
|
||||
|
||||
if file_url:
|
||||
self._file_url = file_url
|
||||
else:
|
||||
self._file_url = f'https://raw.githubusercontent.com/vinifmor/bauh-files/master/appimage/suggestions.txt'
|
||||
|
||||
@property
|
||||
def cached_file_path(self) -> str:
|
||||
if not self._cached_file_path:
|
||||
self._cached_file_path = f'{APPIMAGE_CACHE_DIR}/suggestions.txt'
|
||||
|
||||
return self._cached_file_path
|
||||
|
||||
@property
|
||||
def cached_ts_file_path(self) -> str:
|
||||
if not self._cached_ts_file_path:
|
||||
self._cached_ts_file_path = f'{APPIMAGE_CACHE_DIR}/suggestions.ts'
|
||||
|
||||
return self._cached_ts_file_path
|
||||
|
||||
def register_task(self):
|
||||
self.taskman.register_task(id_=self.task_id,
|
||||
label=self.i18n['task.download_suggestions'], icon_path=get_icon_path())
|
||||
|
||||
def is_custom_local_file_mapped(self) -> bool:
|
||||
return self._file_url and self._file_url.startswith('/')
|
||||
|
||||
def should_download(self, appimage_config: dict) -> bool:
|
||||
if not self._file_url:
|
||||
self.logger.error("No AppImage suggestions file URL defined")
|
||||
return False
|
||||
|
||||
if self.is_custom_local_file_mapped():
|
||||
return False
|
||||
|
||||
try:
|
||||
exp_hours = int(appimage_config['suggestions']['expiration'])
|
||||
except:
|
||||
self.logger.error("An exception happened while trying to parse 'suggestions.expiration'")
|
||||
self.logger.error("An exception happened while trying to parse the AppImage 'suggestions.expiration'")
|
||||
traceback.print_exc()
|
||||
return True
|
||||
|
||||
if exp_hours <= 0:
|
||||
self.logger.info("Suggestions cache is disabled")
|
||||
self.logger.info("The AppImage suggestions cache is disabled")
|
||||
return True
|
||||
|
||||
if not os.path.exists(SUGGESTIONS_CACHED_FILE):
|
||||
self.logger.info("'{}' not found. It must be downloaded".format(SUGGESTIONS_CACHED_FILE))
|
||||
if not os.path.exists(self.cached_file_path):
|
||||
self.logger.info(f"File {self.cached_file_path} not found. It must be downloaded")
|
||||
return True
|
||||
|
||||
if not os.path.exists(SUGGESTIONS_CACHED_TS_FILE):
|
||||
self.logger.info("'{}' not found. The suggestions file must be downloaded.")
|
||||
if not os.path.exists(self.cached_ts_file_path):
|
||||
self.logger.info(f"File {self.cached_ts_file_path} not found. The suggestions file must be downloaded.")
|
||||
return True
|
||||
|
||||
with open(SUGGESTIONS_CACHED_TS_FILE) as f:
|
||||
with open(self.cached_ts_file_path) as f:
|
||||
timestamp_str = f.read()
|
||||
|
||||
try:
|
||||
suggestions_timestamp = datetime.fromtimestamp(float(timestamp_str))
|
||||
except:
|
||||
self.logger.error('Could not parse the cached suggestions timestamp: {}'.format(timestamp_str))
|
||||
self.logger.error(f'Could not parse the cached AppImage suggestions timestamp: {timestamp_str}')
|
||||
traceback.print_exc()
|
||||
return True
|
||||
|
||||
update = suggestions_timestamp + timedelta(hours=exp_hours) <= datetime.utcnow()
|
||||
return update
|
||||
|
||||
def read(self) -> Optional[List[str]]:
|
||||
self.logger.info("Checking if suggestions should be downloaded")
|
||||
def read(self) -> Generator[str, None, None]:
|
||||
if not self._file_url:
|
||||
self.logger.error("No AppImage suggestions file URL defined")
|
||||
yield from ()
|
||||
|
||||
self.logger.info("Checking if AppImage suggestions should be downloaded")
|
||||
if self.should_download(self.config):
|
||||
suggestions_timestamp = datetime.utcnow().timestamp()
|
||||
suggestions_str = self.download()
|
||||
|
||||
Thread(target=self.cache_suggestions, args=(suggestions_str, suggestions_timestamp), daemon=True).start()
|
||||
else:
|
||||
self.logger.info("Reading cached suggestions from '{}'".format(SUGGESTIONS_CACHED_FILE))
|
||||
with open(SUGGESTIONS_CACHED_FILE) as f:
|
||||
if self.is_custom_local_file_mapped():
|
||||
file_path, log_ref = self._file_url, 'local'
|
||||
else:
|
||||
file_path, log_ref = self.cached_file_path, 'cached'
|
||||
|
||||
self.logger.info(f"Reading {log_ref} AppImage suggestions from {file_path}")
|
||||
with open(file_path) as f:
|
||||
suggestions_str = f.read()
|
||||
|
||||
return self.map_suggestions(suggestions_str) if suggestions_str else None
|
||||
yield from self.map_suggestions(suggestions_str) if suggestions_str else ()
|
||||
|
||||
def cache_suggestions(self, text: str, timestamp: float):
|
||||
self.logger.info("Caching suggestions to '{}'".format(SUGGESTIONS_FILE))
|
||||
self.logger.info(f"Caching AppImage suggestions to {self.cached_file_path}")
|
||||
|
||||
cache_dir = os.path.dirname(SUGGESTIONS_CACHED_FILE)
|
||||
cache_dir = os.path.dirname(self.cached_file_path)
|
||||
|
||||
try:
|
||||
Path(cache_dir).mkdir(parents=True, exist_ok=True)
|
||||
cache_dir_ok = True
|
||||
except OSError:
|
||||
self.logger.error("Could not create cache directory '{}'".format(cache_dir))
|
||||
self.logger.error(f"Could not create the caching directory {cache_dir}")
|
||||
traceback.print_exc()
|
||||
cache_dir_ok = False
|
||||
|
||||
if cache_dir_ok:
|
||||
try:
|
||||
with open(SUGGESTIONS_CACHED_FILE, 'w+') as f:
|
||||
with open(self.cached_file_path, 'w+') as f:
|
||||
f.write(text)
|
||||
except:
|
||||
self.logger.error("An exception happened while writing the file '{}'".format(SUGGESTIONS_FILE))
|
||||
self.logger.error(f"An exception happened while writing AppImage suggestions to {self.cached_file_path}")
|
||||
traceback.print_exc()
|
||||
|
||||
try:
|
||||
with open(SUGGESTIONS_CACHED_TS_FILE, 'w+') as f:
|
||||
with open(self.cached_ts_file_path, 'w+') as f:
|
||||
f.write(str(timestamp))
|
||||
except:
|
||||
self.logger.error("An exception happened while writing the file '{}'".format(SUGGESTIONS_CACHED_TS_FILE))
|
||||
self.logger.error(f"An exception happened while writing the cached AppImage suggestions timestamp "
|
||||
f"to {self.cached_ts_file_path}")
|
||||
traceback.print_exc()
|
||||
|
||||
def download(self) -> Optional[str]:
|
||||
self.logger.info("Downloading suggestions from {}".format(SUGGESTIONS_FILE))
|
||||
if not self._file_url:
|
||||
self.logger.error("No AppImage suggestions file URL defined")
|
||||
return
|
||||
|
||||
if self.is_custom_local_file_mapped():
|
||||
self.logger.warning("Local AppImage suggestions file mapped. Nothing will be downloaded")
|
||||
return
|
||||
|
||||
self.logger.info(f"Downloading AppImage suggestions from {self._file_url}")
|
||||
|
||||
try:
|
||||
res = self.http_client.get(SUGGESTIONS_FILE)
|
||||
res = self.http_client.get(self._file_url)
|
||||
except requests.exceptions.ConnectionError:
|
||||
self.logger.warning("Could not download suggestion from '{}'".format(SUGGESTIONS_FILE))
|
||||
self.logger.warning(f"Could not download suggestion from {self._file_url}")
|
||||
return
|
||||
|
||||
if not res:
|
||||
self.logger.warning("Could not download suggestion from '{}'".format(SUGGESTIONS_FILE))
|
||||
self.logger.warning(f"Could not download suggestion from {self._file_url}")
|
||||
return
|
||||
|
||||
if not res.text:
|
||||
self.logger.warning("No suggestion found in {}".format(SUGGESTIONS_FILE))
|
||||
self.logger.warning(f"No AppImage suggestion found in {self._file_url}")
|
||||
return
|
||||
|
||||
return res.text
|
||||
|
||||
def map_suggestions(self, text: str) -> List[str]:
|
||||
return [line for line in text.split('\n') if line]
|
||||
def map_suggestions(self, text: str) -> Generator[str, None, None]:
|
||||
return (line for line in text.split('\n') if line)
|
||||
|
||||
def run(self):
|
||||
if self.create_config:
|
||||
self.taskman.update_progress(self.task_id, 0, self.i18n['task.waiting_task'].format(bold(self.create_config.task_name)))
|
||||
self.create_config.join()
|
||||
self.config = self.create_config.config
|
||||
|
||||
ti = time.time()
|
||||
self.taskman.update_progress(self.task_id, 1, None)
|
||||
|
||||
self.logger.info("Checking if suggestions should be downloaded")
|
||||
should_download = self.should_download(self.config)
|
||||
self.taskman.update_progress(self.task_id, 30, None)
|
||||
if not self.is_custom_local_file_mapped():
|
||||
if self.create_config:
|
||||
wait_msg = self.i18n['task.waiting_task'].format(bold(self.create_config.task_name))
|
||||
self.taskman.update_progress(self.task_id, 0, wait_msg)
|
||||
self.create_config.join()
|
||||
self.config = self.create_config.config
|
||||
|
||||
try:
|
||||
if should_download:
|
||||
suggestions_timestamp = datetime.utcnow().timestamp()
|
||||
suggestions_str = self.download()
|
||||
self.taskman.update_progress(self.task_id, 70, None)
|
||||
ti = time.time()
|
||||
self.taskman.update_progress(self.task_id, 1, None)
|
||||
|
||||
if suggestions_str:
|
||||
self.cache_suggestions(suggestions_str, suggestions_timestamp)
|
||||
else:
|
||||
self.logger.info("Cached suggestions are up-to-date")
|
||||
except:
|
||||
self.logger.error("An unexpected exception happened")
|
||||
traceback.print_exc()
|
||||
self.logger.info("Checking if AppImage suggestions should be downloaded")
|
||||
should_download = self.should_download(self.config)
|
||||
self.taskman.update_progress(self.task_id, 30, None)
|
||||
|
||||
try:
|
||||
if should_download:
|
||||
suggestions_timestamp = datetime.utcnow().timestamp()
|
||||
suggestions_str = self.download()
|
||||
self.taskman.update_progress(self.task_id, 70, None)
|
||||
|
||||
if suggestions_str:
|
||||
self.cache_suggestions(suggestions_str, suggestions_timestamp)
|
||||
else:
|
||||
self.logger.info("Cached AppImage suggestions are up-to-date")
|
||||
except:
|
||||
self.logger.error("An unexpected exception happened while downloading AppImage suggestions")
|
||||
traceback.print_exc()
|
||||
|
||||
self.taskman.update_progress(self.task_id, 100, None)
|
||||
self.taskman.finish_task(self.task_id)
|
||||
tf = time.time()
|
||||
self.logger.info("Took {0:.9f} seconds to download suggestions".format(tf - ti))
|
||||
self.logger.info(f"Took {tf - ti:.9f} seconds to download suggestions")
|
||||
|
||||
Reference in New Issue
Block a user