Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES/8041.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed ``ArtifactFileField.pre_save`` spuriously rejecting artifact uploads whose filenames start with ``artifact`` when ``MEDIA_ROOT`` is empty (S3/Azure object-storage backends). The ``startswith`` guard now short-circuits when ``MEDIA_ROOT`` is empty, preventing false detection of fresh uploads as already-stored artifacts.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

attack of the double backticks

7 changes: 6 additions & 1 deletion pulpcore/app/models/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,12 @@ def pre_save(self, model_instance, add):
artifact_storage_path,
os.path.join(settings.MEDIA_ROOT, artifact_storage_path),
]
is_in_artifact_storage = file.name.startswith(os.path.join(settings.MEDIA_ROOT, "artifact"))
# Guard against empty MEDIA_ROOT (object-storage backends such as S3/Azure set it to ""),
# where os.path.join("", "artifact") == "artifact" and any filename starting with
# "artifact" would be falsely detected as already residing in artifact storage.
is_in_artifact_storage = bool(settings.MEDIA_ROOT) and file.name.startswith(
os.path.join(settings.MEDIA_ROOT, "artifact")
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is short-circuiting actually what we want here? MEDIA_ROOT is always empty on S3 and Azure (I think) so how does it determine if an artifact exists with those backends?


if not already_in_place and is_in_artifact_storage:
raise ValueError(
Expand Down
40 changes: 40 additions & 0 deletions pulpcore/tests/functional/api/test_artifact_presave_gh8041.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""
Regression test for https://github.com/pulp/pulpcore/issues/8041

``ArtifactFileField.pre_save`` used a raw ``startswith`` against ``settings.MEDIA_ROOT``
to detect files already in artifact storage. When ``MEDIA_ROOT`` is ``""``
(S3/Azure object-storage backends), ``os.path.join("", "artifact") == "artifact"``,
so any upload whose filename started with ``"artifact"`` was falsely detected as
already-stored and raised ``ValueError`` → HTTP 500.
"""

import os

import pytest


@pytest.mark.parametrize(
"filename",
[
"artifact-foo-1.0-1.noarch.rpm",
"artifact-bar.tar.gz",
],
)
def test_artifact_upload_artifact_prefix_filename_when_media_root_empty(
pulpcore_bindings, tmp_path, pulp_settings, filename
):
"""Upload a file whose name starts with ``artifact`` — must succeed on object-storage backends.

This test only applies when ``MEDIA_ROOT`` is empty (object-storage backends such
as S3 and Azure). On filesystem backends the bug does not occur, so the test is
skipped to avoid false positives.
"""
if pulp_settings.MEDIA_ROOT:
pytest.skip("Bug GH-8041 only affects backends where MEDIA_ROOT is empty (S3/Azure).")

@dralley dralley Sep 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It definitely shouldn't be in a file named pulpcore/tests/functional/api/test_artifact_presave_gh8041.py

I'm not sure I like having it so narrowly scoped either. TBH for such a specific bug we probably do not need a regression test, but more testing in general would be welcome if there are gaps.


temp_file = tmp_path / filename
temp_file.write_bytes(os.urandom(32))

# Before the fix this raised HTTP 500 (ValueError in ArtifactFileField.pre_save).
artifact = pulpcore_bindings.ArtifactsApi.create(str(temp_file))
assert artifact.pulp_href is not None
Loading