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.models.Result import Result, ResultError from core.services.helpers.download_file import download_file_and_verify 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 download_path = application_version.download_path target_file_hash = application_version.file_hash if application_version_observer is not None: application_version_observer.notify('downloading', f"Downloading {target_app_name} {target_app_version}. Connecting..") download_result = download_file_and_verify( target_app_name=target_app_name, download_path=download_path, target_file_hash=target_file_hash, application_version_observer=application_version_observer, target_app_version=target_app_version, connection_observer=connection_observer ) ################################################ # IT WORKED - SAVE IT ################################################ if download_result.valid: response_buffer = download_result.data file_hash = download_result.message 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') ################################################ # FAILED ################################################ else: if download_result.error_type == ResultError.CONNECTION: raise ConnectionError(f'Could not connect, to download {target_app_name}.') elif download_result.error_type == ResultError.INVALID_INPUT: raise FileIntegrityError('Application version file integrity could not be verified.') else: return None # legacy: # @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() # def _get_httpx_client(target_app_name: str, connection_observer: Optional[ConnectionObserver]) -> httpx.Client: # connection_type = ConfigurationController.get_connection_enum() # did_it_work = connect.make_client(connection_type, connection_observer) # always gets boolean # if not did_it_work: # raise ConnectionError(f'Could not connect, to download {target_app_name}.') # else: # client = httpx_client.get_http_session() # logger.info(f"client type is {type(client)}") # return client # @staticmethod # def get_all(application: Optional[Application] = None): # return ApplicationVersion.all(application) # 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.')