from core.services.networking.httpx import httpx_client from core.services.networking.httpx import connect from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType from core.controllers.ConfigurationController import ConfigurationController from core.models.Configuration import Configuration, ConnectionChoice from core.Errors import FileIntegrityError, UnsupportedApplicationVersionError, ApplicationAlreadyInstalledError from core.controllers.ApplicationController import ApplicationController from core.models.session.Application import Application # from core.models.session.ApplicationVersion import ApplicationVersion from core.models.orm_models.ApplicationVersion import ApplicationVersion from core.models.manage.wrapper import safe_db_operation, WrapperRollback from core.models.DatabaseOperation import DatabaseOperation, DBErrorType from core.models.orm_calls.application_version_calls import get_application_version, execute_get_all from core.observers.ApplicationVersionObserver import ApplicationVersionObserver from core.observers.ConnectionObserver import ConnectionObserver from core.services.WebServiceApiService import WebServiceApiService from core.errors.logger import logger import httpx from io import BytesIO from typing import Optional import hashlib import shutil import tarfile from sqlalchemy.orm import Session from sqlalchemy import select import os class ApplicationVersionController: @staticmethod def get(application_code: str, version_number: str): return get_application_version(application_code, version_number) @staticmethod def get_all(application: Optional[Application] = None): database_object = execute_get_all() if database_object.valid: return database_object.data else: logger.error(f"[Application Version Controller] Got invalid SQL Query which could not be solved by the wrapper, with error message {database_object.message} and type {database_object.error_type}") return None @staticmethod def install(application_version: ApplicationVersion, reinstall: bool = False, application_version_observer: Optional[ApplicationVersionObserver] = None, connection_observer: Optional[ConnectionObserver] = None): if not application_version.is_supported: raise UnsupportedApplicationVersionError('The application version in question is not supported.') if reinstall: ApplicationVersionController.uninstall(application_version) if application_version.is_installed(): raise ApplicationAlreadyInstalledError('The application in question is already installed.') # this used to go through "with_preferred_connection", but now re-uses the same HTTPx client as sync, ApplicationVersionController.__install(application_version, application_version_observer, connection_observer) # legacy: # from core.controllers.ConnectionController import ConnectionController # ConnectionController.with_preferred_connection(application_version, task=ApplicationVersionController.__install, application_version_observer=application_version_observer, connection_observer=connection_observer) @staticmethod def uninstall(application_version: ApplicationVersion): shutil.rmtree(application_version.get_installation_path(), ignore_errors=True) @staticmethod def _sync(proxies: Optional[dict] = None): applications = ApplicationController.get_all() application_versions = [] for application_code in (application.code for application in applications): application_version_subset = WebServiceApiService.get_application_versions(application_code, proxies) for application_version in application_version_subset: application_versions.append(application_version) ApplicationVersion.truncate() ApplicationVersion.save_many(application_versions) @staticmethod def __install(application_version: ApplicationVersion, application_version_observer: Optional[ApplicationVersionObserver] = None, connection_observer: Optional[ConnectionObserver] = None): target_app_name = application_version.application_code.capitalize() target_app_version = application_version.version_number if application_version_observer is not None: application_version_observer.notify('downloading', f"Downloading {target_app_name} {target_app_version}. Connecting..") # legacy: # application_version_observer.notify('downloading', application_version) ################################################ # SETUP HTTP CLIENT ################################################ client = httpx_client.get_http_session() logger.info(f"client type is {type(client)}") if client is None: client = _get_httpx_client(target_app_name=target_app_name, connection_observer=connection_observer) ################################################ # GET THE DATA ################################################ download_path = application_version.download_path logger.info(f"download_path is {download_path}") with client.stream('GET', download_path) as response: logger.info("doing the stream...") if response.status_code == 200: response_size = int(response.headers.get('Content-Length', 0)) response_buffer = BytesIO() block_size = 1024 bytes_written = 0 for data in response.iter_bytes(block_size): bytes_written += len(data) response_buffer.write(data) progress = (bytes_written / response_size) * 100 if response_size > 0 else 0 if application_version_observer is not None: application_version_observer.notify('download_progressing', f"Downloading {target_app_name} {progress:.2f}% v: {target_app_version}") else: raise ConnectionError('The application version could not be downloaded.') application_version_observer.notify('downloaded', f"Downloaded {target_app_name} {target_app_version}") response_buffer.seek(0) ################################################ # VERIFY THE HASH ################################################ file_hash = ApplicationVersionController.__calculate_file_hash(response_buffer) if file_hash != application_version.file_hash: raise FileIntegrityError('Application version file integrity could not be verified.') with tarfile.open(fileobj=response_buffer, mode = 'r:gz') as tar_file: tar_file.extractall(application_version.get_installation_path()) with open(f'{application_version.get_installation_path()}/.sha3-512', 'w') as hash_file: hash_file.write(f'{file_hash}\n') @staticmethod def __calculate_file_hash(file): hasher = hashlib.sha3_512() buffer = file.read(65536) while len(buffer) > 0: hasher.update(buffer) buffer = file.read(65536) file.seek(0) return hasher.hexdigest() # This function is a temporary transition for the connect module to get better public APIs def _get_httpx_client(target_app_name: str, connection_observer: Optional[ConnectionObserver]) -> httpx.Client: connection_type = ConfigurationController.get_connection_enum() client = connect.make_client(connection_type, connection_observer) logger.info(f"client type is {type(client)}") if isinstance(client, ApiResponse): if not client.valid: raise ConnectionError(f'Could not connect, to download {target_app_name}.') if isinstance(client, bool): if not client: raise ConnectionError(f'Could not connect, to download {target_app_name}.') else: logger.info("It's a boolean, getting the client now.") client = httpx_client.get_http_session() logger.info(f"client type is {type(client)}") return client # legacy: # @staticmethod # def get_all(application: Optional[Application] = None): # return ApplicationVersion.all(application)