diff --git a/artemis/resources.py b/artemis/resources.py index 5d0e7a0..fcdb427 100644 --- a/artemis/resources.py +++ b/artemis/resources.py @@ -7302,7 +7302,7 @@ qt_resource_struct = b"\ \x00\x00\x03X\x00\x02\x00\x00\x00\x04\x00\x00\x00+\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x04\xc4\x00\x01\x00\x00\x00\x01\x00\x01v\x14\ -\x00\x00\x01\x90\x0eD\xa3A\ +\x00\x00\x01\x90\x0eV\xebx\ \x00\x00\x05d\x00\x01\x00\x00\x00\x01\x00\x01\x8f*\ \x00\x00\x01\x90\x01\x93J\xb0\ \x00\x00\x04\xe6\x00\x01\x00\x00\x00\x01\x00\x01\x7f\x0c\ diff --git a/artemis/ui/artemis.py b/artemis/ui/artemis.py index 8d43e10..f55fe3d 100644 --- a/artemis/ui/artemis.py +++ b/artemis/ui/artemis.py @@ -7,7 +7,7 @@ from artemis.utils.constants import Constants, Messages from artemis.utils.sys_utils import open_directory, make_tar, unpack_tar from artemis.utils.sql_utils import ArtemisDatabase, ArtemisSignal from artemis.utils.path_utils import DATA_DIR -from artemis.utils.network_utils import NetworkManager +from artemis.utils.update_utils import UpdateManager from artemis.utils.generic_utils import generate_filter_query from artemis.utils.path_utils import normalize_dialog_path from artemis.utils.config_utils import CONFIGURE_QT @@ -62,13 +62,12 @@ class UIArtemis(QObject): # Creating istances for other windows self.preferences = UIPreferences(self) self.dbmanager = UIdbmanager(self) - self.downloader = UIDownloader(self) self.spaceweather = UIspaceweather(self) self.docmanager = UIdocumentsmanager(self) self.sigeditor = UIsignaleditor(self) self.cateditor = UIcategoryeditor(self) - self.network_manager = NetworkManager(self) + self.update_manager = UpdateManager(self) self.autoload_db() @@ -217,15 +216,19 @@ class UIArtemis(QObject): def check_update_db(self): """ User manual check for updates db updates """ - self.network_manager.show_popup = True - self.network_manager.check_updates() + self.update_manager.check_updates(True) def start_download_db(self): """ Show the downloader and start the download of the sigid db """ - self.downloader.show_ui.emit() - self.downloader.on_start() + self.downloader = UIDownloader(self) + self.downloader.finished.connect(self.update_manager.post_download_db) + self.downloader.on_start( + self.update_manager.remote_db_url, + self.update_manager.remote_db_size, + DATA_DIR + ) def dialog_download_db(self, message_type, title, message): diff --git a/artemis/ui/downloader.py b/artemis/ui/downloader.py index d3a38a6..99f42ad 100644 --- a/artemis/ui/downloader.py +++ b/artemis/ui/downloader.py @@ -2,14 +2,12 @@ from PySide6.QtQml import QQmlApplicationEngine from PySide6.QtCore import QObject, Slot, Signal, QUrl, QSaveFile, QDir, QIODevice from PySide6.QtNetwork import QNetworkReply, QNetworkRequest, QNetworkAccessManager -from artemis.utils.config_utils import * -from artemis.utils.sys_utils import delete_file, delete_dir, match_hash, unpack_tar from artemis.utils.constants import Messages -from artemis.utils.path_utils import DATA_DIR class UIDownloader(QObject): # Python > QML Signals + finished = Signal() show_ui = Signal() close_ui = Signal() update_progress_bar = Signal(int, int) @@ -25,6 +23,9 @@ class UIDownloader(QObject): self._engine.load('qrc:/ui/Downloader.qml') self._window = self._engine.rootObjects()[0] + self.file_url = None + self.file_size = None + self._connect() @@ -39,20 +40,22 @@ class UIDownloader(QObject): self.update_status.connect(self._window.updateStatus) - @Slot() - def on_start(self): + def on_start(self, url, filesize, save_path): """ Start the download of the DB taking the needed url and size from the attributes of the UpdatesController class """ - url_file = QUrl(self._parent.network_manager.remote_db_url) - dest_path = QDir(DATA_DIR) - self.dest_file = dest_path.filePath(url_file.fileName()) + self.show_ui.emit() + + self.file_url = QUrl(url) + self.file_size = filesize + dest_path = QDir(save_path) + self.dest_file = dest_path.filePath(self.file_url.fileName()) self.file = QSaveFile(self.dest_file) if self.file.open(QIODevice.WriteOnly): # Start a GET HTTP request self.manager = QNetworkAccessManager(self) - self.reply = self.manager.get(QNetworkRequest(url_file)) + self.reply = self.manager.get(QNetworkRequest(self.file_url)) self.reply.downloadProgress.connect(self.on_progress) self.reply.finished.connect(self.on_finished) self.reply.readyRead.connect(self.on_ready_read) @@ -74,8 +77,6 @@ class UIDownloader(QObject): if self.file: self.file.cancelWriting() - self.close_ui.emit() - @Slot() def on_ready_read(self): @@ -95,25 +96,19 @@ class UIDownloader(QObject): if self.file: self.file.commit() + + if self.reply.error() == QNetworkReply.NoError: + self.finished.emit() - self.update_status.emit("Checking DB integrity (SHA-256)") - - if match_hash(self.dest_file, self._parent.network_manager.remote_db_hash): - self.update_status.emit("Unpacking archive...") - delete_dir(DATA_DIR / 'SigID') - unpack_tar(self.dest_file, DATA_DIR / 'SigID') - delete_file(self.dest_file) - self._parent.load_db('SigID') - self.close_ui.emit() + self.close_ui.emit() @Slot(int, int) def on_progress(self, bytesReceived: int): """ Update progress bar and label """ - total_bytes = self._parent.network_manager.remote_db_size - self.update_status.emit("{:.1f} Mb / {:.1f} Mb".format(bytesReceived/10**6, total_bytes/10**6)) - self.update_progress_bar.emit(bytesReceived, total_bytes) + self.update_status.emit("{:.1f} Mb / {:.1f} Mb".format(bytesReceived/10**6, self.file_size/10**6)) + self.update_progress_bar.emit(bytesReceived, self.file_size) @Slot(QNetworkReply.NetworkError) diff --git a/artemis/utils/constants.py b/artemis/utils/constants.py index a7f610f..c79a9de 100644 --- a/artemis/utils/constants.py +++ b/artemis/utils/constants.py @@ -39,6 +39,7 @@ class Messages: UP_TO_DATE = "You're up to date!" DB_NEW_VER = "New SigID DB version available!" ART_NEW_VER = "New Artemis version available!" + DB_CORRUPTED = "Database Corruption Detected" # Messages DB_CREATION_SUCCESS_MSG = "The new database has been created succesfully." @@ -51,7 +52,8 @@ class Messages: UP_TO_DATE_MSG = "The latest version of Artemis and SigID wiki is installed on your computer." DB_NEW_VER_MSG = "A new version of the database ({}) is available for download. Download now?" ART_NEW_VER_MSG = "A new version of Artemis ({}) is available for download. Check GitHub page now?" - DOWNLOAD_CORRUPTED_MSG = "Downloaded data corrupted or invalid. Please retry." + DB_CORRUPTED_MSG = "Downloaded data corrupted or invalid. Please retry." + DB_DOWNLOAD_SUCCESS_MSG = "The database has been successfully downloaded and is now being loaded." class Query(): diff --git a/artemis/utils/update_utils.py b/artemis/utils/update_utils.py new file mode 100644 index 0000000..5f67a0e --- /dev/null +++ b/artemis/utils/update_utils.py @@ -0,0 +1,174 @@ +import os +import requests + +from packaging.version import Version + +from artemis.utils.constants import Constants, Messages +from artemis.utils.sql_utils import ArtemisDatabase +from artemis.utils.sys_utils import is_windows, is_linux, is_macos, delete_file, delete_dir, match_hash, unpack_tar +from artemis.utils.path_utils import DATA_DIR + + +class UpdateManager: + """ Class used to manage DB and software updates + """ + + def __init__(self, parent): + self._parent = parent + self.sigid_db_path = DATA_DIR / 'SigID' / Constants.SQL_NAME + + self.db_update = None + self.art_update = None + + self.remote_db_url = None + self.remote_db_hash = None + self.remote_db_version = None + self.remote_db_size = None + self.remote_db_file_name = None + + self.remote_art_version = None + + self.check_updates() + + + def check_updates(self, show_popup=False): + """ Checks if a software or DB update is available. + Prioritize Artemis update over DB one. + + Args: + show_popup (bool, optional): + Suppress the "already up-to-date" message on startup. + Defaults to False. + """ + latest_json = self._fetch_remote_json(Constants.LATEST_VERSION_URL, show_popup) + if latest_json: + local_db = self._load_local_db() + remote_db = latest_json['sigID_DB'] + + self.remote_db_version = remote_db['version'] + self.remote_db_url = remote_db['url'] + self.remote_db_hash = remote_db['sha256_hash'] + self.remote_db_size = remote_db['total_bytes'] + self.remote_db_file_name = self.remote_db_url.split('/')[-1] + + if is_windows(): + self.remote_art_version = latest_json['windows']['version'] + elif is_linux(): + self.remote_art_version = latest_json['linux']['version'] + elif is_macos(): + self.remote_art_version = latest_json['mac']['version'] + + if Version(self.remote_art_version) > Version(Constants.APPLICATION_VERSION): + self.art_update = True + else: + self.art_update = False + + if self.art_update: + self._show_popup_art_update() + else: + if local_db: + if self.remote_db_version > local_db.version: + self._show_popup_db_update() + elif show_popup: + self._show_popup_up_to_date() + else: + self._show_popup_initial_db_download() + + + def _fetch_remote_json(self, url, show_popup=False): + """ Fetches the remote json from a url + """ + try: + response = requests.get(url) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + if show_popup: + self._parent.dialog_popup( + Messages.DIALOG_TYPE_ERROR, + Messages.NO_CONNECTION, + Messages.NO_CONNECTION_MSG.format(e) + ) + return None + + + def _load_local_db(self): + """ Loads the local database if exists + """ + if os.path.exists(self.sigid_db_path): + local_db = ArtemisDatabase('SigID') + local_db.load() + return local_db + return None + + + def post_download_db(self): + latest_db_tar_path = DATA_DIR / self.remote_db_file_name + if match_hash(latest_db_tar_path, self.remote_db_hash): + delete_dir(DATA_DIR / 'SigID') + unpack_tar(latest_db_tar_path, DATA_DIR / 'SigID') + self._parent.load_db('SigID') + self._show_popup_db_download_complete() + else: + self._show_popup_db_hash_failed() + delete_file(latest_db_tar_path) + + + def _show_popup_db_update(self): + """ Prompts the user to download the updated version of the database. + """ + self._parent.dialog_download_db( + Messages.DIALOG_TYPE_WARN, + Messages.DB_NEW_VER, + Messages.DB_NEW_VER_MSG.format(self.remote_db_version) + ) + + + def _show_popup_art_update(self): + """ Prompts the user to download the updated version of the database. + """ + self._parent.dialog_download_artemis( + Messages.DIALOG_TYPE_WARN, + Messages.ART_NEW_VER, + Messages.ART_NEW_VER_MSG.format(self.remote_art_version) + ) + + + def _show_popup_up_to_date(self): + """ Notifies the user that the database is up to date. + """ + self._parent.dialog_popup( + Messages.DIALOG_TYPE_INFO, + Messages.UP_TO_DATE, + Messages.UP_TO_DATE_MSG + ) + + + def _show_popup_initial_db_download(self): + """ Prompts the user to download the database for the first time. + """ + self._parent.dialog_download_db( + Messages.DIALOG_TYPE_QUEST, + Messages.NO_DB_DETECTED, + Messages.NO_DB_DETECTED_MSG + ) + + + def _show_popup_db_download_complete(self): + """ DB has been succesfully downloaded + """ + self._parent.dialog_popup( + Messages.DIALOG_TYPE_INFO, + Messages.GENERIC_SUCCESS, + Messages.DB_DOWNLOAD_SUCCESS_MSG + ) + + + def _show_popup_db_hash_failed(self): + """ Notify the user after detection of a corrupted database + """ + self._parent.dialog_popup( + Messages.DIALOG_TYPE_ERROR, + Messages.DB_CORRUPTED, + Messages.DB_CORRUPTED_MSG + )