diff --git a/src/backend/core/recording/event/parsers.py b/src/backend/core/recording/event/parsers.py index f99a07b0..1bdf3995 100644 --- a/src/backend/core/recording/event/parsers.py +++ b/src/backend/core/recording/event/parsers.py @@ -6,7 +6,7 @@ import re from dataclasses import dataclass from functools import lru_cache from typing import Any, Dict, Optional, Protocol -from urllib.parse import quote_plus +from urllib.parse import quote from django.conf import settings from django.utils.module_loading import import_string @@ -165,8 +165,9 @@ class S3Parser(BaseS3Parser): if not filepath: raise ParsingEventDataError("Missing object key name") filetype, _ = mimetypes.guess_type(filepath) - # Preserve already URL-encoded separators while encoding raw S3 object keys. - filepath = quote_plus(filepath, safe="%") + # Normalize raw S3-compatible object keys without re-encoding + # already encoded AWS S3 notification keys. + filepath = quote(filepath, safe="%+") return StorageEvent( filepath=filepath, filetype=filetype, diff --git a/src/backend/core/tests/recording/event/test_parsers.py b/src/backend/core/tests/recording/event/test_parsers.py index be2d2b47..e48b5bce 100644 --- a/src/backend/core/tests/recording/event/test_parsers.py +++ b/src/backend/core/tests/recording/event/test_parsers.py @@ -406,6 +406,32 @@ def test_s3_parser_accepts_unencoded_filepath(settings): assert parser.get_recording_id(data) == recording_id +def test_s3_parser_preserves_plus_signs_in_raw_filepath(settings): + """Test S3 parser preserves plus signs while encoding slash separators.""" + settings.RECORDING_OUTPUT_FOLDER = "recordings" + + recording_id = "80ae9fe5-639a-438b-b86e-9e3dd2d55f4d" + parser = S3Parser(bucket_name="recordings-bucket") + + data = { + "Records": [ + { + "s3": { + "bucket": {"name": "recordings-bucket"}, + "object": { + "key": f"folder+name/recordings/{recording_id}.mp4", + }, + } + } + ] + } + + event = parser.parse(data) + + assert event.filepath == f"folder+name%2Frecordings%2F{recording_id}.mp4" + assert parser.validate(event) == recording_id + + def test_s3_get_recording_id_success(s3_parser, valid_s3_event): """Test successful extraction of recording ID from S3 event.""" recording_id = s3_parser.get_recording_id(valid_s3_event)