From a22bf95bceb676793dc9f259fb359cf3ac07330f Mon Sep 17 00:00:00 2001 From: Manuel Raynaud Date: Thu, 27 Feb 2025 16:19:17 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=92=EF=B8=8F(back)=20set=20ContentDisp?= =?UTF-8?q?osition=20on=20media=20upload?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On the media upload endpoint, we want to set the content-disposition header. Its value is based on the uploaded file mime-type and if flagged as unsafe. If the file is not an image or is unsafe then the contentDisposition is set to attachment to force its download. Otherwise, we set it to inline. --- src/backend/core/api/serializers.py | 2 ++ src/backend/core/api/viewsets.py | 13 +++++++++++++ .../test_api_documents_attachment_upload.py | 4 ++++ 3 files changed, 19 insertions(+) diff --git a/src/backend/core/api/serializers.py b/src/backend/core/api/serializers.py index c16ecd4d..a90990c2 100644 --- a/src/backend/core/api/serializers.py +++ b/src/backend/core/api/serializers.py @@ -418,6 +418,7 @@ class FileUploadSerializer(serializers.Serializer): self.context["expected_extension"] = extension self.context["content_type"] = magic_mime_type + self.context["file_name"] = file.name return file @@ -426,6 +427,7 @@ class FileUploadSerializer(serializers.Serializer): attrs["expected_extension"] = self.context["expected_extension"] attrs["is_unsafe"] = self.context["is_unsafe"] attrs["content_type"] = self.context["content_type"] + attrs["file_name"] = self.context["file_name"] return attrs diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index 0a5ee4cc..596d26dc 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -928,6 +928,19 @@ class DocumentViewSet( key = f"{document.key_base}/{ATTACHMENTS_FOLDER:s}/{file_id!s}{file_unsafe}.{extension:s}" + file_name = serializer.validated_data["file_name"] + if ( + not serializer.validated_data["content_type"].startswith("image/") + or serializer.validated_data["is_unsafe"] + ): + extra_args.update( + {"ContentDisposition": f'attachment; filename="{file_name:s}"'} + ) + else: + extra_args.update( + {"ContentDisposition": f'inline; filename="{file_name:s}"'} + ) + file = serializer.validated_data["file"] default_storage.connection.meta.client.upload_fileobj( file, default_storage.bucket_name, key, ExtraArgs=extra_args diff --git a/src/backend/core/tests/documents/test_api_documents_attachment_upload.py b/src/backend/core/tests/documents/test_api_documents_attachment_upload.py index de8d3dca..000d0251 100644 --- a/src/backend/core/tests/documents/test_api_documents_attachment_upload.py +++ b/src/backend/core/tests/documents/test_api_documents_attachment_upload.py @@ -79,6 +79,7 @@ def test_api_documents_attachment_upload_anonymous_success(): assert file_head["Metadata"] == {"owner": "None"} assert file_head["ContentType"] == "image/png" + assert file_head["ContentDisposition"] == 'inline; filename="test.png"' @pytest.mark.parametrize( @@ -217,6 +218,7 @@ def test_api_documents_attachment_upload_success(via, role, mock_user_teams): ) assert file_head["Metadata"] == {"owner": str(user.id)} assert file_head["ContentType"] == "image/png" + assert file_head["ContentDisposition"] == 'inline; filename="test.png"' def test_api_documents_attachment_upload_invalid(client): @@ -303,6 +305,7 @@ def test_api_documents_attachment_upload_fix_extension( ) assert file_head["Metadata"] == {"owner": str(user.id), "is_unsafe": "true"} assert file_head["ContentType"] == content_type + assert file_head["ContentDisposition"] == f'attachment; filename="{name:s}"' def test_api_documents_attachment_upload_empty_file(): @@ -354,3 +357,4 @@ def test_api_documents_attachment_upload_unsafe(): ) assert file_head["Metadata"] == {"owner": str(user.id), "is_unsafe": "true"} assert file_head["ContentType"] == "application/octet-stream" + assert file_head["ContentDisposition"] == 'attachment; filename="script.exe"'