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"'