diff --git a/app/cabinet/routes/admin_news_media.py b/app/cabinet/routes/admin_news_media.py index d126a71e..7aeb639a 100644 --- a/app/cabinet/routes/admin_news_media.py +++ b/app/cabinet/routes/admin_news_media.py @@ -66,6 +66,7 @@ async def upload_media( # Read slightly over the max allowed size so we can detect oversized files. absolute_max_bytes = settings.MEDIA_MAX_VIDEO_SIZE_MB * _BYTES_PER_MB + 1 data = await file.read(absolute_max_bytes) + await file.close() if not data: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -85,7 +86,7 @@ async def upload_media( raise HTTPException( status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, detail='Unsupported file type. Allowed: JPEG, PNG, WebP, MP4, WebM', - ) + ) from None # Enforce per-type size limits max_size_mb = ( @@ -110,12 +111,12 @@ async def upload_media( ) else: saved = await save_video(data, upload_path) - except Exception: - logger.exception('Failed to save uploaded media', media_type=media_type) + except (ValueError, OSError) as exc: + logger.warning('Failed to save uploaded media', media_type=media_type, error=str(exc)) raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail='Failed to process uploaded file', - ) + ) from None logger.info( 'Media uploaded', diff --git a/app/services/news_media_service.py b/app/services/news_media_service.py index c5aba3fd..c51f779a 100644 --- a/app/services/news_media_service.py +++ b/app/services/news_media_service.py @@ -66,7 +66,10 @@ def ensure_upload_dirs(upload_path: Path) -> None: (upload_path / subdir).mkdir(parents=True, exist_ok=True) -def detect_file_type(data: bytes) -> tuple[str, str]: +MediaType = Literal['image', 'video'] + + +def detect_file_type(data: bytes) -> tuple[MediaType, str]: """Detect media type and extension from magic bytes. Returns: @@ -88,8 +91,10 @@ def detect_file_type(data: bytes) -> tuple[str, str]: if data[: len(signature)] == signature: return 'image', ext - # Check MP4: bytes 4-7 must be 'ftyp' - if len(data) >= 8 and data[4:8] == b'ftyp': + # Check MP4: bytes 4-7 must be 'ftyp', bytes 8-12 must be a known video brand. + # Rejects HEIC/HEIF images (ftypheic, ftypmif1, etc.) which share the ftyp box format. + _MP4_VIDEO_BRANDS = {b'isom', b'mp41', b'mp42', b'M4V ', b'avc1', b'iso5', b'iso6', b'mmp4', b'dash', b'mp71'} + if data[4:8] == b'ftyp' and data[8:12] in _MP4_VIDEO_BRANDS: return 'video', '.mp4' # Check standard video signatures @@ -170,7 +175,7 @@ def _process_and_save_image( except Exception: tmp_thumb.unlink(missing_ok=True) # Non-fatal: log and continue without thumbnail - logger.warning('Failed to generate thumbnail', filename=filename) + logger.warning('Failed to generate thumbnail', filename=filename, exc_info=True) thumbnail_filename = None relative_path = f'{_IMAGES_DIR}/{filename}'