diff --git a/.github/workflows/impress.yml b/.github/workflows/impress.yml index 896f07ef..385aa633 100644 --- a/.github/workflows/impress.yml +++ b/.github/workflows/impress.yml @@ -206,10 +206,11 @@ jobs: - name: Install development dependencies run: pip install --user .[dev] - - name: Install gettext (required to compile messages) + - name: Install gettext (required to compile messages) and MIME support run: | sudo apt-get update - sudo apt-get install -y gettext pandoc + sudo apt-get install -y gettext pandoc shared-mime-info + sudo wget https://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types -O /etc/mime.types - name: Generate a MO file from strings extracted from the project run: python manage.py compilemessages diff --git a/CHANGELOG.md b/CHANGELOG.md index 0805656a..da6a6826 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to - github actions to managed Crowdin workflow - 📈Integrate Posthog #540 - +- 🏷️(backend) add content-type to uploaded files #552 ## Changed diff --git a/Dockerfile b/Dockerfile index 941f0747..6547f0b6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -76,6 +76,8 @@ RUN apk add \ pango \ shared-mime-info +RUN wget https://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types -O /etc/mime.types + # Copy entrypoint COPY ./docker/files/usr/local/bin/entrypoint /usr/local/bin/entrypoint diff --git a/src/backend/core/api/serializers.py b/src/backend/core/api/serializers.py index d5b0d4c7..e2369f49 100644 --- a/src/backend/core/api/serializers.py +++ b/src/backend/core/api/serializers.py @@ -388,6 +388,7 @@ class FileUploadSerializer(serializers.Serializer): raise serializers.ValidationError("Could not determine file extension.") self.context["expected_extension"] = extension + self.context["content_type"] = magic_mime_type return file @@ -395,6 +396,7 @@ class FileUploadSerializer(serializers.Serializer): """Override validate to add the computed extension to validated_data.""" attrs["expected_extension"] = self.context["expected_extension"] attrs["is_unsafe"] = self.context["is_unsafe"] + attrs["content_type"] = self.context["content_type"] return attrs diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index 02b0f277..10adee35 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -605,7 +605,10 @@ class DocumentViewSet( key = f"{document.key_base}/{ATTACHMENTS_FOLDER:s}/{file_id!s}.{extension:s}" # Prepare metadata for storage - extra_args = {"Metadata": {"owner": str(request.user.id)}} + extra_args = { + "Metadata": {"owner": str(request.user.id)}, + "ContentType": serializer.validated_data["content_type"], + } if serializer.validated_data["is_unsafe"]: extra_args["Metadata"]["is_unsafe"] = "true" 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 1288f8ca..4a6564d6 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 @@ -64,12 +64,22 @@ def test_api_documents_attachment_upload_anonymous_success(): assert response.status_code == 201 pattern = re.compile(rf"^/media/{document.id!s}/attachments/(.*)\.png") - match = pattern.search(response.json()["file"]) + file_path = response.json()["file"] + match = pattern.search(file_path) file_id = match.group(1) # Validate that file_id is a valid UUID uuid.UUID(file_id) + # Now, check the metadata of the uploaded file + key = file_path.replace("/media", "") + file_head = default_storage.connection.meta.client.head_object( + Bucket=default_storage.bucket_name, Key=key + ) + + assert file_head["Metadata"] == {"owner": "None"} + assert file_head["ContentType"] == "image/png" + @pytest.mark.parametrize( "reach, role", @@ -206,6 +216,7 @@ def test_api_documents_attachment_upload_success(via, role, mock_user_teams): Bucket=default_storage.bucket_name, Key=key ) assert file_head["Metadata"] == {"owner": str(user.id)} + assert file_head["ContentType"] == "image/png" def test_api_documents_attachment_upload_invalid(client): @@ -247,16 +258,18 @@ def test_api_documents_attachment_upload_size_limit_exceeded(settings): @pytest.mark.parametrize( - "name,content,extension", + "name,content,extension,content_type", [ - ("test.exe", b"text", "exe"), - ("test", b"text", "txt"), - ("test.aaaaaa", b"test", "txt"), - ("test.txt", PIXEL, "txt"), - ("test.py", b"#!/usr/bin/python", "py"), + ("test.exe", b"text", "exe", "text/plain"), + ("test", b"text", "txt", "text/plain"), + ("test.aaaaaa", b"test", "txt", "text/plain"), + ("test.txt", PIXEL, "txt", "image/png"), + ("test.py", b"#!/usr/bin/python", "py", "text/plain"), ], ) -def test_api_documents_attachment_upload_fix_extension(name, content, extension): +def test_api_documents_attachment_upload_fix_extension( + name, content, extension, content_type +): """ A file with no extension or a wrong extension is accepted and the extension is corrected in storage. @@ -287,6 +300,7 @@ def test_api_documents_attachment_upload_fix_extension(name, content, extension) Bucket=default_storage.bucket_name, Key=key ) assert file_head["Metadata"] == {"owner": str(user.id), "is_unsafe": "true"} + assert file_head["ContentType"] == content_type def test_api_documents_attachment_upload_empty_file(): @@ -335,3 +349,4 @@ def test_api_documents_attachment_upload_unsafe(): Bucket=default_storage.bucket_name, Key=key ) assert file_head["Metadata"] == {"owner": str(user.id), "is_unsafe": "true"} + assert file_head["ContentType"] == "application/octet-stream"