Skip to content
Closed
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
18 changes: 17 additions & 1 deletion src/sentry/api/endpoints/debug_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -859,7 +859,23 @@ def _clone_proguard_debug_file_for_reupload(
}

meta = build_proguard_reupload_dif_meta(debug_file, requested_debug_id)
if debug_file.file is not None and not debug_file.uses_objectstore_for_read():

if features.has("organizations:objectstore-debugfiles-exclusive-write", project.organization):
# Exclusive writes always produce an Objectstore-only clone, regardless
# of whether the source is File-backed, dual-written, or Objectstore-only.
checksum = debug_file.get_checksum()
file_size = debug_file.get_file_size()
Comment thread
lcian marked this conversation as resolved.
source_fileobj = debug_file.get_file()
try:
with tempfile.TemporaryFile() as tmp:
shutil.copyfileobj(source_fileobj, tmp)
tmp.seek(0)
dif, created = create_objectstore_dif_from_id(
project, meta, tmp, checksum, file_size
)
finally:
source_fileobj.close()
elif debug_file.file is not None and not debug_file.uses_objectstore_for_read():
# Legacy File-backed source (and dual-written source when Objectstore
# reads are disabled): reuse the existing File row under a new debug ID.
dif, created = create_dif_from_id(project, meta, file=debug_file.file)
Expand Down
33 changes: 22 additions & 11 deletions src/sentry/models/debugfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,20 @@ def create_objectstore_dif_from_id(
return dif, True


def _checksum_and_size(fileobj: IO[bytes]) -> tuple[str, int]:
"""Returns ``(sha1_hex, size)`` for ``fileobj`` and rewinds it to the start."""
file_size = 0
h = hashlib.sha1()
while True:
chunk = fileobj.read(16384)
if not chunk:
break
h.update(chunk)
file_size += len(chunk)
fileobj.seek(0, 0)
return h.hexdigest(), file_size


def create_dif_from_id(
project: Project,
meta: DifMeta,
Expand Down Expand Up @@ -641,16 +655,7 @@ def create_dif_from_id(
checksum = file.checksum
assert checksum is not None
elif fileobj is not None:
file_size = 0
h = hashlib.sha1()
while True:
chunk = fileobj.read(16384)
if not chunk:
break
h.update(chunk)
file_size += len(chunk)
checksum = h.hexdigest()
fileobj.seek(0, 0)
checksum, file_size = _checksum_and_size(fileobj)
else:
raise RuntimeError("missing file object")

Expand Down Expand Up @@ -1021,7 +1026,13 @@ def create_debug_file_from_dif(
rv = []
for meta in to_create:
with open(meta.path, "rb") as f:
dif, created = create_dif_from_id(project, meta, fileobj=f)
if features.has(
"organizations:objectstore-debugfiles-exclusive-write", project.organization
):
checksum, file_size = _checksum_and_size(f)
dif, created = create_objectstore_dif_from_id(project, meta, f, checksum, file_size)
else:
dif, created = create_dif_from_id(project, meta, fileobj=f)
if created:
rv.append(dif)
return rv
Expand Down
27 changes: 27 additions & 0 deletions tests/sentry/api/endpoints/test_debug_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,33 @@ def test_project_debug_files_role_overrides_organization(self) -> None:
self._assert_successful_download(response, PROGUARD_SOURCE)


@requires_objectstore
class DebugFileObjectstoreExclusiveUploadTest(DebugFilesTestCases):
"""Zip/dSYM uploads under organizations:objectstore-debugfiles-exclusive-write."""

def test_exclusive_write_zip_upload_is_objectstore_only(self) -> None:
with self.feature(
{
"organizations:objectstore-debugfiles-exclusive-write": True,
"organizations:objectstore-debugfiles-write": False,
"organizations:objectstore-debugfiles-read": True,
}
):
response = self._upload_proguard(self.url, PROGUARD_UUID)

assert response.status_code == 201, response.content
assert len(response.data) == 1
assert response.data[0]["uuid"] == PROGUARD_UUID
assert response.data[0]["sha1"] == "e6d3c5185dac63eddfdc1a5edfffa32d46103b44"

dif = ProjectDebugFile.objects.get(project_id=self.project.id, debug_id=PROGUARD_UUID)
assert dif.file_id is None
assert dif.storage_path is not None
assert dif.content_type == "text/x-proguard+plain"
assert dif.get_file().read() == PROGUARD_SOURCE
assert not File.objects.filter(type="project.dif").exists()


@requires_objectstore
class DebugFileObjectstoreRedirectTest(DebugFilesTestCases):
"""Explicit coverage of both redirect branches for Objectstore-backed debug files."""
Expand Down
49 changes: 49 additions & 0 deletions tests/sentry/api/endpoints/test_dif_assemble.py
Original file line number Diff line number Diff line change
Expand Up @@ -650,3 +650,52 @@ def test_clone_dual_written_source_to_file(self) -> None:
assert second_dif.file_id is not None
assert second_dif.storage_path is None
assert second_dif.get_file().read() == file_contents

def test_clone_file_backed_source_to_objectstore_exclusive(self) -> None:
"""A file-backed source is cloned under exclusive-write as Objectstore-only."""

file_contents = b"proguard mapping"
checksum = sha1(file_contents).hexdigest()
blob = FileBlob.from_file_with_organization(ContentFile(file_contents), self.organization)
chunks = [blob.checksum]

with self.feature(
{
"organizations:objectstore-debugfiles-exclusive-write": False,
"organizations:objectstore-debugfiles-write": False,
}
):
self._assemble_source(checksum, chunks)

first_dif = ProjectDebugFile.objects.get(
project_id=self.project.id,
debug_id="00000000-0000-0000-0000-000000000000",
)
assert first_dif.file_id is not None
assert first_dif.storage_path is None

with self.feature(
{
"organizations:objectstore-debugfiles-exclusive-write": True,
"organizations:objectstore-debugfiles-write": False,
"organizations:objectstore-debugfiles-read": True,
}
):
response = self._clone_request(checksum, chunks)

assert response.status_code == 200, response.content
assert response.data[checksum]["state"] == ChunkFileState.OK
assert response.data[checksum]["dif"]["uuid"] == "11111111-1111-1111-1111-111111111111"

second_dif = ProjectDebugFile.objects.get(
project_id=self.project.id,
debug_id="11111111-1111-1111-1111-111111111111",
)
# Source stays file-backed; clone is Objectstore-only.
first_dif.refresh_from_db()
assert first_dif.file_id is not None
assert first_dif.storage_path is None
assert second_dif.file_id is None
assert second_dif.storage_path is not None
assert second_dif.get_file().read() == file_contents
assert File.objects.filter(type="project.dif", checksum=checksum).count() == 1
Loading