import mimetypes from typing import Any, Dict, Tuple from django.conf import settings from django.core.exceptions import ValidationError from django.db import transaction from django.utils import timezone from vbv_lernwelt.core.models import User from vbv_lernwelt.files.enums import FileUploadStorage from vbv_lernwelt.files.integrations import s3_generate_presigned_post from vbv_lernwelt.files.models import UploadFile from vbv_lernwelt.files.utils import ( bytes_to_mib, file_generate_local_upload_url, file_generate_name, file_generate_upload_path, ) def _validate_file_size(file_obj): max_size = settings.FILE_MAX_SIZE if file_obj.size > max_size: raise ValidationError( f"File is too large. It should not exceed {bytes_to_mib(max_size)} MiB" ) class FileStandardUploadService: """ This also serves as an example of a service class, which encapsulates 2 different behaviors (create & update) under a namespace. Meaning, we use the class here for: 1. The namespace 2. The ability to reuse `_infer_file_name_and_type` (which can also be an util) """ def __init__(self, user: User, file_obj): self.user = user self.file_obj = file_obj def _infer_file_name_and_type( self, file_name: str = "", file_type: str = "" ) -> Tuple[str, str]: if not file_name: file_name = self.file_obj.name if not file_type: guessed_file_type, encoding = mimetypes.guess_type(file_name) if guessed_file_type is None: file_type = "" else: file_type = guessed_file_type return file_name, file_type @transaction.atomic def create(self, file_name: str = "", file_type: str = "") -> UploadFile: _validate_file_size(self.file_obj) file_name, file_type = self._infer_file_name_and_type(file_name, file_type) obj = UploadFile( file=self.file_obj, original_file_name=file_name, file_name=file_generate_name(file_name), file_type=file_type, uploaded_by=self.user, upload_finished_at=timezone.now(), ) obj.full_clean() obj.save() return obj @transaction.atomic def update( self, file: UploadFile, file_name: str = "", file_type: str = "" ) -> UploadFile: _validate_file_size(self.file_obj) file_name, file_type = self._infer_file_name_and_type(file_name, file_type) file.file = self.file_obj file.original_file_name = file_name file.file_name = file_generate_name(file_name) file.file_type = file_type file.uploaded_by = self.user file.upload_finished_at = timezone.now() file.full_clean() file.save() return file class FileDirectUploadService: """ This also serves as an example of a service class, which encapsulates a flow (start & finish) + one-off action (upload_local) into a namespace. Meaning, we use the class here for: 1. The namespace """ def __init__(self, user: User): self.user = user @transaction.atomic def start( self, file_name: str, file_type: str ) -> Tuple[UploadFile, Dict[str, Any]]: file = UploadFile( original_file_name=file_name, file_name=file_generate_name(file_name), file_type=file_type, uploaded_by=self.user, file=None, ) file.full_clean() file.save() upload_path = file_generate_upload_path(file, file.file_name) """ We are doing this in order to have an associated file for the field. """ file.file = file.file.field.attr_class(file, file.file.field, upload_path) file.save() if settings.FILE_UPLOAD_STORAGE == FileUploadStorage.S3.value: pre_signed_data = s3_generate_presigned_post( file_path=upload_path, file_type=file.file_type, file_name=file_name ) else: pre_signed_data = { "url": file_generate_local_upload_url(file_id=str(file.id)), } return file, pre_signed_data @transaction.atomic def finish(self, *, file: UploadFile) -> UploadFile: # Potentially, check against user file.upload_finished_at = timezone.now() file.full_clean() file.save() return file @transaction.atomic def upload_local(self, *, file: UploadFile, file_obj) -> UploadFile: _validate_file_size(file_obj) # Potentially, check against user file.file = file_obj file.full_clean() file.save() return file