mirror of
https://github.com/spalencsar/bearhub.git
synced 2026-07-07 01:14:15 +02:00
485 lines
19 KiB
Python
485 lines
19 KiB
Python
import glob
|
|
import json
|
|
import logging
|
|
import os
|
|
import tarfile
|
|
import time
|
|
import traceback
|
|
from datetime import datetime, timedelta, timezone
|
|
from pathlib import Path
|
|
from threading import Thread
|
|
from typing import Optional, Generator
|
|
|
|
import requests
|
|
|
|
from bauh.api.abstract.handler import TaskManager, ProcessWatcher
|
|
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
|
|
from bauh.gems.appimage.model import AppImage
|
|
from bauh.view.util.translation import I18n
|
|
|
|
|
|
class DatabaseUpdater(Thread):
|
|
COMPRESS_FILE_PATH = f'{APPIMAGE_CACHE_DIR}/db.tar.gz'
|
|
|
|
def __init__(self, i18n: I18n, http_client: HttpClient, logger: logging.Logger, taskman: TaskManager,
|
|
watcher: Optional[ProcessWatcher] = None, appimage_config: Optional[dict] = None, create_config: Optional[CreateConfigFile] = None):
|
|
super(DatabaseUpdater, self).__init__(daemon=True)
|
|
self.http_client = http_client
|
|
self.logger = logger
|
|
self.i18n = i18n
|
|
self.taskman = taskman
|
|
self.watcher = watcher
|
|
self.task_id = 'appim_db'
|
|
self.config = appimage_config
|
|
self.create_config = create_config
|
|
self.taskman.register_task(self.task_id, self.i18n['appimage.task.db_update'], get_icon_path())
|
|
|
|
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(f'{APPIMAGE_CACHE_DIR}/*')}
|
|
|
|
if not files:
|
|
self.logger.warning(f'No database files on {APPIMAGE_CACHE_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), tz=timezone.utc)
|
|
except Exception:
|
|
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.now(timezone.utc)
|
|
self.logger.info('Finished. Took {0:.2f} seconds'.format(time.time() - ti))
|
|
return update
|
|
|
|
def _update_task_progress(self, progress: float, substatus: Optional[str] = None):
|
|
self.taskman.update_progress(self.task_id, progress, substatus)
|
|
|
|
if self.watcher:
|
|
self.watcher.change_substatus(substatus)
|
|
|
|
def download_databases(self) -> bool:
|
|
self._update_task_progress(10, self.i18n['appimage.update_database.downloading'])
|
|
self.logger.info('Retrieving AppImage databases')
|
|
|
|
database_timestamp = datetime.now(timezone.utc).timestamp()
|
|
try:
|
|
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 not res:
|
|
self.logger.warning('Could not download the database file {}'.format(URL_COMPRESSED_DATABASES))
|
|
return False
|
|
|
|
Path(APPIMAGE_CACHE_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, self.i18n['appimage.update_database.deleting_old'])
|
|
old_db_files = glob.glob(f'{APPIMAGE_CACHE_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, self.i18n['appimage.update_database.uncompressing'])
|
|
self.logger.info('Uncompressing {}'.format(self.COMPRESS_FILE_PATH))
|
|
|
|
try:
|
|
tf = tarfile.open(self.COMPRESS_FILE_PATH)
|
|
tf.extractall(APPIMAGE_CACHE_DIR)
|
|
self.logger.info('Successfully uncompressed file {}'.format(self.COMPRESS_FILE_PATH))
|
|
except Exception:
|
|
self.logger.error('Could not extract file {}'.format(self.COMPRESS_FILE_PATH))
|
|
traceback.print_exc()
|
|
return False
|
|
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")
|
|
|
|
return True
|
|
|
|
def run(self):
|
|
ti = time.time()
|
|
|
|
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
|
|
|
|
self.taskman.update_progress(self.task_id, 1, self.i18n['appimage.task.db_update.checking'])
|
|
|
|
if self.should_update(self.config):
|
|
self.download_databases()
|
|
|
|
self.taskman.update_progress(self.task_id, 100, None)
|
|
self.taskman.finish_task(self.task_id)
|
|
tf = time.time()
|
|
self.logger.info("Finished. Took {0:.2f} seconds".format(tf - ti))
|
|
|
|
|
|
class SymlinksVerifier(Thread):
|
|
|
|
def __init__(self, taskman: TaskManager, i18n: I18n, logger: logging.Logger):
|
|
super(SymlinksVerifier, self).__init__(daemon=True)
|
|
self.taskman = taskman
|
|
self.i18n = i18n
|
|
self.logger = logger
|
|
self.task_id = 'appim_symlink_check'
|
|
self.taskman.register_task(self.task_id, self.i18n['appimage.task.symlink_check'], get_icon_path())
|
|
|
|
@staticmethod
|
|
def create_symlink(app: AppImage, file_path: str, logger: logging.Logger, watcher: ProcessWatcher = None):
|
|
logger.info("Creating a symlink for '{}'".format(app.name))
|
|
possible_names = (app.get_clean_name(), '{}-appimage'.format(app.get_clean_name()), app.name.lower(), '{}-appimage'.format(app.name.lower()))
|
|
|
|
if os.path.exists(SYMLINKS_DIR) and not os.path.isdir(SYMLINKS_DIR):
|
|
logger.warning("'{}' is not a directory. It will not be possible to create a symlink for '{}'".format(SYMLINKS_DIR, app.name))
|
|
return
|
|
|
|
available_system_dirs = (SYMLINKS_DIR, *(l for l in ('/usr/bin', '/usr/local/bin') if os.path.isdir(l)))
|
|
|
|
# checking if the link already exists:
|
|
|
|
available_name = None
|
|
for name in possible_names:
|
|
available_name = name
|
|
for sysdir in available_system_dirs:
|
|
if os.path.exists('{}/{}'.format(sysdir, name)):
|
|
available_name = None
|
|
break
|
|
|
|
if available_name:
|
|
break
|
|
|
|
if not available_name:
|
|
msg = "It was not possible to create a symlink for '{}' because the names {} are already available on the system".format(app.name,
|
|
possible_names)
|
|
logger.warning(msg)
|
|
if watcher:
|
|
watcher.print('[warning] {}'.format(msg))
|
|
else:
|
|
try:
|
|
Path(SYMLINKS_DIR).mkdir(parents=True, exist_ok=True)
|
|
except Exception:
|
|
logger.error("Could not create symlink directory '{}'".format(SYMLINKS_DIR))
|
|
return
|
|
|
|
symlink_path = '{}/{}'.format(SYMLINKS_DIR, available_name)
|
|
|
|
try:
|
|
os.symlink(src=file_path, dst=symlink_path)
|
|
app.symlink = symlink_path
|
|
|
|
msg = "symlink successfully created at {}".format(symlink_path)
|
|
logger.info(msg)
|
|
|
|
if watcher:
|
|
watcher.print(msg)
|
|
except Exception:
|
|
msg = "Could not create the symlink '{}'".format(symlink_path)
|
|
logger.error(msg)
|
|
|
|
if watcher:
|
|
watcher.print('[error] {}'.format(msg))
|
|
|
|
def run(self):
|
|
if os.path.exists(INSTALLATION_DIR):
|
|
installed_files = glob.glob(f'{INSTALLATION_DIR}/*/*.json')
|
|
|
|
if installed_files:
|
|
self.logger.info("Checking installed AppImage files with no symlinks created")
|
|
|
|
progress_per_file = (1/len(installed_files)) * 100
|
|
total_progress = 0
|
|
for json_file in installed_files:
|
|
with open(json_file) as f:
|
|
try:
|
|
data = json.loads(f.read())
|
|
except Exception:
|
|
self.logger.warning("Could not parse data from '{}'".format(json_file))
|
|
data = None
|
|
|
|
if data and not data.get('symlink'):
|
|
if not data.get('install_dir'):
|
|
data['install_dir'] = '/'.join(json_file.split('/')[0:-1])
|
|
|
|
app = AppImage(**data, i18n=self.i18n)
|
|
|
|
file_path = util.find_appimage_file(app.install_dir)
|
|
|
|
if file_path:
|
|
self.create_symlink(app, file_path, self.logger)
|
|
data['symlink'] = app.symlink
|
|
|
|
# caching
|
|
try:
|
|
with open(json_file, 'w+') as f:
|
|
f.write(json.dumps(data))
|
|
except Exception:
|
|
self.logger.warning("Could not update cached data on '{}'".format(json_file))
|
|
traceback.print_exc()
|
|
|
|
else:
|
|
self.logger.warning("No AppImage file found on installation dir '{}'".format(file_path))
|
|
|
|
total_progress += progress_per_file
|
|
self.taskman.update_progress(self.task_id, total_progress, '')
|
|
|
|
self.taskman.update_progress(self.task_id, 100, '')
|
|
self.taskman.finish_task(self.task_id)
|
|
return
|
|
|
|
self.logger.info("No AppImage applications found. Aborting")
|
|
self.taskman.update_progress(self.task_id, 100, '')
|
|
self.taskman.finish_task(self.task_id)
|
|
|
|
|
|
class AppImageSuggestionsDownloader(Thread):
|
|
|
|
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
|
|
self.i18n = i18n
|
|
self.http_client = http_client
|
|
self.taskman = taskman
|
|
self.config = appimage_config
|
|
self.task_id = 'appim.suggestions'
|
|
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 Exception:
|
|
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("The AppImage suggestions cache is disabled")
|
|
return True
|
|
|
|
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(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(self.cached_ts_file_path) as f:
|
|
timestamp_str = f.read()
|
|
|
|
try:
|
|
suggestions_timestamp = datetime.fromtimestamp(float(timestamp_str), tz=timezone.utc)
|
|
except Exception:
|
|
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.now(timezone.utc)
|
|
return update
|
|
|
|
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.now(timezone.utc).timestamp()
|
|
suggestions_str = self.download()
|
|
|
|
Thread(target=self.cache_suggestions, args=(suggestions_str, suggestions_timestamp), daemon=True).start()
|
|
else:
|
|
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()
|
|
|
|
yield from self.map_suggestions(suggestions_str) if suggestions_str else ()
|
|
|
|
def cache_suggestions(self, text: str, timestamp: float):
|
|
self.logger.info(f"Caching AppImage suggestions to {self.cached_file_path}")
|
|
|
|
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(f"Could not create the caching directory {cache_dir}")
|
|
traceback.print_exc()
|
|
cache_dir_ok = False
|
|
|
|
if cache_dir_ok:
|
|
try:
|
|
with open(self.cached_file_path, 'w+') as f:
|
|
f.write(text)
|
|
except Exception:
|
|
self.logger.error(f"An exception happened while writing AppImage suggestions to {self.cached_file_path}")
|
|
traceback.print_exc()
|
|
|
|
try:
|
|
with open(self.cached_ts_file_path, 'w+') as f:
|
|
f.write(str(timestamp))
|
|
except Exception:
|
|
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]:
|
|
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(self._file_url)
|
|
except requests.exceptions.ConnectionError:
|
|
self.logger.warning(f"Could not download suggestion from {self._file_url}")
|
|
return
|
|
|
|
if not res:
|
|
self.logger.warning(f"Could not download suggestion from {self._file_url}")
|
|
return
|
|
|
|
if not res.text:
|
|
self.logger.warning(f"No AppImage suggestion found in {self._file_url}")
|
|
return
|
|
|
|
return res.text
|
|
|
|
def map_suggestions(self, text: str) -> Generator[str, None, None]:
|
|
return (line for line in text.split('\n') if line)
|
|
|
|
def run(self):
|
|
ti = time.time()
|
|
|
|
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
|
|
|
|
ti = time.time()
|
|
self.taskman.update_progress(self.task_id, 1, None)
|
|
|
|
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.now(timezone.utc).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 Exception:
|
|
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(f"Took {tf - ti:.9f} seconds to download suggestions")
|