sp-hydra-veil-core/core/controllers/ApplicationVersionController.py

184 lines
8.2 KiB
Python

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)