🏷️(backend) add content-type to uploaded files
All the uploaded files had the content-type set to `application/octet-stream`. It create issues when the file is downloaded from the frontend because the browser doesn't know how to handle the file. We now determine the content-type of the file and set it to the file object.
This commit is contained in:
5
.github/workflows/impress.yml
vendored
5
.github/workflows/impress.yml
vendored
@@ -206,10 +206,11 @@ jobs:
|
|||||||
- name: Install development dependencies
|
- name: Install development dependencies
|
||||||
run: pip install --user .[dev]
|
run: pip install --user .[dev]
|
||||||
|
|
||||||
- name: Install gettext (required to compile messages)
|
- name: Install gettext (required to compile messages) and MIME support
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
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
|
- name: Generate a MO file from strings extracted from the project
|
||||||
run: python manage.py compilemessages
|
run: python manage.py compilemessages
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ and this project adheres to
|
|||||||
|
|
||||||
- github actions to managed Crowdin workflow
|
- github actions to managed Crowdin workflow
|
||||||
- 📈Integrate Posthog #540
|
- 📈Integrate Posthog #540
|
||||||
|
- 🏷️(backend) add content-type to uploaded files #552
|
||||||
|
|
||||||
## Changed
|
## Changed
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ RUN apk add \
|
|||||||
pango \
|
pango \
|
||||||
shared-mime-info
|
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 entrypoint
|
||||||
COPY ./docker/files/usr/local/bin/entrypoint /usr/local/bin/entrypoint
|
COPY ./docker/files/usr/local/bin/entrypoint /usr/local/bin/entrypoint
|
||||||
|
|
||||||
|
|||||||
@@ -388,6 +388,7 @@ class FileUploadSerializer(serializers.Serializer):
|
|||||||
raise serializers.ValidationError("Could not determine file extension.")
|
raise serializers.ValidationError("Could not determine file extension.")
|
||||||
|
|
||||||
self.context["expected_extension"] = extension
|
self.context["expected_extension"] = extension
|
||||||
|
self.context["content_type"] = magic_mime_type
|
||||||
|
|
||||||
return file
|
return file
|
||||||
|
|
||||||
@@ -395,6 +396,7 @@ class FileUploadSerializer(serializers.Serializer):
|
|||||||
"""Override validate to add the computed extension to validated_data."""
|
"""Override validate to add the computed extension to validated_data."""
|
||||||
attrs["expected_extension"] = self.context["expected_extension"]
|
attrs["expected_extension"] = self.context["expected_extension"]
|
||||||
attrs["is_unsafe"] = self.context["is_unsafe"]
|
attrs["is_unsafe"] = self.context["is_unsafe"]
|
||||||
|
attrs["content_type"] = self.context["content_type"]
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -605,7 +605,10 @@ class DocumentViewSet(
|
|||||||
key = f"{document.key_base}/{ATTACHMENTS_FOLDER:s}/{file_id!s}.{extension:s}"
|
key = f"{document.key_base}/{ATTACHMENTS_FOLDER:s}/{file_id!s}.{extension:s}"
|
||||||
|
|
||||||
# Prepare metadata for storage
|
# 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"]:
|
if serializer.validated_data["is_unsafe"]:
|
||||||
extra_args["Metadata"]["is_unsafe"] = "true"
|
extra_args["Metadata"]["is_unsafe"] = "true"
|
||||||
|
|
||||||
|
|||||||
@@ -64,12 +64,22 @@ def test_api_documents_attachment_upload_anonymous_success():
|
|||||||
assert response.status_code == 201
|
assert response.status_code == 201
|
||||||
|
|
||||||
pattern = re.compile(rf"^/media/{document.id!s}/attachments/(.*)\.png")
|
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)
|
file_id = match.group(1)
|
||||||
|
|
||||||
# Validate that file_id is a valid UUID
|
# Validate that file_id is a valid UUID
|
||||||
uuid.UUID(file_id)
|
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(
|
@pytest.mark.parametrize(
|
||||||
"reach, role",
|
"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
|
Bucket=default_storage.bucket_name, Key=key
|
||||||
)
|
)
|
||||||
assert file_head["Metadata"] == {"owner": str(user.id)}
|
assert file_head["Metadata"] == {"owner": str(user.id)}
|
||||||
|
assert file_head["ContentType"] == "image/png"
|
||||||
|
|
||||||
|
|
||||||
def test_api_documents_attachment_upload_invalid(client):
|
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(
|
@pytest.mark.parametrize(
|
||||||
"name,content,extension",
|
"name,content,extension,content_type",
|
||||||
[
|
[
|
||||||
("test.exe", b"text", "exe"),
|
("test.exe", b"text", "exe", "text/plain"),
|
||||||
("test", b"text", "txt"),
|
("test", b"text", "txt", "text/plain"),
|
||||||
("test.aaaaaa", b"test", "txt"),
|
("test.aaaaaa", b"test", "txt", "text/plain"),
|
||||||
("test.txt", PIXEL, "txt"),
|
("test.txt", PIXEL, "txt", "image/png"),
|
||||||
("test.py", b"#!/usr/bin/python", "py"),
|
("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
|
A file with no extension or a wrong extension is accepted and the extension
|
||||||
is corrected in storage.
|
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
|
Bucket=default_storage.bucket_name, Key=key
|
||||||
)
|
)
|
||||||
assert file_head["Metadata"] == {"owner": str(user.id), "is_unsafe": "true"}
|
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():
|
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
|
Bucket=default_storage.bucket_name, Key=key
|
||||||
)
|
)
|
||||||
assert file_head["Metadata"] == {"owner": str(user.id), "is_unsafe": "true"}
|
assert file_head["Metadata"] == {"owner": str(user.id), "is_unsafe": "true"}
|
||||||
|
assert file_head["ContentType"] == "application/octet-stream"
|
||||||
|
|||||||
Reference in New Issue
Block a user