from __future__ import annotations import pathlib from collections.abc import Callable from dataclasses import dataclass import pyminizip from app.adapters.aws.s3 import S3Client from app.adapters.ows_dmp import AudienceExport, AudienceExportStatus, OwsDmpClient from app.adapters.ows_notifications import ( OwsNotificationsClient, ) from app.exceptions import HandlerError @dataclass class ExportAudienceFileRequest: bucket: str key: str @dataclass class UploadedZipFile: dst_file: pathlib.Path filename: str key: str @dataclass class ExportAudienceFileHandler: s3_client: S3Client ows_dmp_client: OwsDmpClient ows_notifications_client: OwsNotificationsClient tmp_dir: pathlib.Path generate_password_func: Callable[[], str] def handle(self, request: ExportAudienceFileRequest) -> UploadedZipFile: # Get audience export id and filename by S3 Key *other, export_id, filename = request.key.split("/") # Set export status 'PROCESSING' export = self.update_export( export_id=export_id, status=AudienceExportStatus.PROCESSING, ) # Generate safe password password = self.generate_password_func() try: uploaded_file = self.upload_secured_zip_file( export_id, filename, bucket=request.bucket, key=request.key, password=password, ) except Exception as exc: # Set export status 'FAILED' self.update_export( export_id=export_id, status=AudienceExportStatus.FAILED, ) raise HandlerError( f"Failed to upload secured zip file `{request.key}`.", code="UPLOAD_ERROR", ) from exc # Try to send notification with password try: self.ows_notifications_client.notify_audience_file_exported( identity_id=export.created_by, audience_name=export.audience_name, filename=uploaded_file.filename, password=password, ) except Exception as exc: # Set export status 'FAILED' self.update_export( export_id=export_id, status=AudienceExportStatus.FAILED, ) raise HandlerError( f"Failed to send exported file notification: {exc}.", code="NOTIFICATION_SEND_ERROR", ) from exc # Set export status 'COMPLETED' self.update_export( export_id=export_id, status=AudienceExportStatus.COMPLETED, zip_key=uploaded_file.key, ) return uploaded_file def update_export( self, export_id: str, status: AudienceExportStatus, zip_key: str | None = None ) -> AudienceExport: try: return self.ows_dmp_client.update_audience_export( export_id=export_id, status=status, zip_key=zip_key ) except Exception as exc: raise HandlerError( f"Failed to change export `{export_id}` status to `{status}`.", code="UPDATE_EXPORT_ERROR", ) from exc def upload_secured_zip_file( self, export_id: str, filename: str, *, bucket: str, key: str, password: str ) -> UploadedZipFile: zip_filename = f"{filename}.zip" # Download file from S3 to tmp directory src_file = self.tmp_dir.joinpath(filename) with src_file.open("wb") as file_obj: self.s3_client.download_fileobj( bucket=bucket, key=key, file_obj=file_obj, ) # Compress downloaded file and compress with password dst_file = self.tmp_dir.joinpath(zip_filename) pyminizip.compress(str(src_file), None, str(dst_file), password, 5) # Upload compressed file to S3 upload_key = f"zip/{export_id}/{zip_filename}" # Upload created zip file with dst_file.open("rb") as file_obj: self.s3_client.upload_fileobj( bucket=bucket, key=upload_key, file_obj=file_obj, ) src_file.unlink(missing_ok=True) dst_file.unlink(missing_ok=True) return UploadedZipFile(dst_file=dst_file, filename=zip_filename, key=upload_key)