From 3804f7524ad5e6eefacb7a38b3bbc0ddc3467f01 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 e9616aff..340456b2 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -925,6 +925,19 @@ class DocumentViewSet( if serializer.validated_data["is_unsafe"]: extra_args["Metadata"]["is_unsafe"] = "true" + 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 4a6564d6..1e4aa758 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): @@ -301,6 +303,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(): @@ -350,3 +353,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"'