diff --git a/migrations_lockfile.txt b/migrations_lockfile.txt index ef2a091184c8..1e114eb2176b 100644 --- a/migrations_lockfile.txt +++ b/migrations_lockfile.txt @@ -31,7 +31,7 @@ replays: 0007_organizationmember_replay_access seer: 0017_drop_old_fk_columns -sentry: 1114_extend_repository_url_length +sentry: 1115_projectdebugfile_add_objectstore_columns social_auth: 0003_social_auth_json_field diff --git a/src/sentry/api/endpoints/debug_files.py b/src/sentry/api/endpoints/debug_files.py index d166be11d35e..ea4c88eb9c47 100644 --- a/src/sentry/api/endpoints/debug_files.py +++ b/src/sentry/api/endpoints/debug_files.py @@ -272,6 +272,7 @@ def download(self, debug_file_id, project: Project): raise Http404 try: + assert debug_file.file is not None fp = debug_file.file.getfile() response = StreamingHttpResponse( iter(lambda: fp.read(4096), b""), content_type="application/octet-stream" diff --git a/src/sentry/migrations/1115_projectdebugfile_add_objectstore_columns.py b/src/sentry/migrations/1115_projectdebugfile_add_objectstore_columns.py new file mode 100644 index 000000000000..c1004654f40d --- /dev/null +++ b/src/sentry/migrations/1115_projectdebugfile_add_objectstore_columns.py @@ -0,0 +1,44 @@ +from django.db import migrations, models + +import sentry.db.models.fields.bounded +import sentry.db.models.fields.foreignkey + +from sentry.new_migrations.migrations import CheckedMigration + + +class Migration(CheckedMigration): + is_post_deployment = False + + dependencies = [ + ("sentry", "1114_extend_repository_url_length"), + ] + + operations = [ + migrations.AlterField( + model_name="projectdebugfile", + name="file", + field=sentry.db.models.fields.foreignkey.FlexibleForeignKey( + null=True, on_delete=models.PROTECT, to="sentry.file" + ), + ), + migrations.AddField( + model_name="projectdebugfile", + name="storage_path", + field=models.TextField(null=True), + ), + migrations.AddField( + model_name="projectdebugfile", + name="content_type", + field=models.TextField(null=True), + ), + migrations.AddField( + model_name="projectdebugfile", + name="file_size", + field=sentry.db.models.fields.bounded.BoundedBigIntegerField(null=True), + ), + migrations.AddField( + model_name="projectdebugfile", + name="date_created", + field=models.DateTimeField(null=True), + ), + ] diff --git a/src/sentry/models/debugfile.py b/src/sentry/models/debugfile.py index 79edf1560d37..ce2c0f6e7bf2 100644 --- a/src/sentry/models/debugfile.py +++ b/src/sentry/models/debugfile.py @@ -119,7 +119,8 @@ def find_by_debug_ids( class ProjectDebugFile(Model): __relocation_scope__ = RelocationScope.Excluded - file = FlexibleForeignKey("sentry.File", on_delete=models.PROTECT) + # When the migration to Objectstore is complete, this can be removed. + file = FlexibleForeignKey("sentry.File", null=True, on_delete=models.PROTECT) checksum = models.CharField(max_length=40, null=True, db_index=True) object_name = models.TextField() cpu_name = models.CharField(max_length=40) @@ -129,6 +130,16 @@ class ProjectDebugFile(Model): data = LegacyTextJSONField(default=dict, null=True) date_accessed = models.DateTimeField(default=timezone.now, db_default=Now()) + # The following fields are present if and only if the file is stored in Objectstore. + # Key of the file in Objectstore. + storage_path = models.TextField(null=True) + # Mirrors `file.headers["Content-Type"]` for files stored in Objectstore. + content_type = models.TextField(null=True) + # Mirrors `file.size` for files stored in Objectstore. + file_size = BoundedBigIntegerField(null=True) + # Mirrors `file.timestamp` for files stored in Objectstore. + date_created = models.DateTimeField(null=True) + objects: ClassVar[ProjectDebugFileManager] = ProjectDebugFileManager() difcache: ClassVar[DIFCache] @@ -145,6 +156,7 @@ class Meta: @property def file_format(self) -> str: + assert self.file is not None ct = self.file.headers.get("Content-Type", "unknown").lower() return KNOWN_DIF_FORMATS.get(ct, "unknown") @@ -201,7 +213,8 @@ def delete(self, *args: Any, **kwargs: Any) -> tuple[int, dict[str, int]]: # row behind, but no surviving ProjectDebugFile should point to a deleted # File. try: - self.file.delete() + if self.file is not None: + self.file.delete() except ProtectedError: pass @@ -224,6 +237,7 @@ def clean_redundant_difs(project: Project, debug_id: str) -> None: uuidmap_seen = False il2cpp_seen = False for i, dif in enumerate(difs): + assert dif.file is not None mime_type = dif.file.headers.get("Content-Type") if mime_type == DIF_MIMETYPES["bcsymbolmap"]: if not bcsymbolmap_seen: @@ -733,6 +747,7 @@ def fetch_difs( except OSError as e: if e.errno != errno.ENOENT: raise + assert dif.file is not None dif.file.save_to(dif_path) rv[debug_id] = dif_path diff --git a/tests/sentry/tasks/test_assemble.py b/tests/sentry/tasks/test_assemble.py index 4d12379d1980..3f55efc7dea0 100644 --- a/tests/sentry/tasks/test_assemble.py +++ b/tests/sentry/tasks/test_assemble.py @@ -92,6 +92,7 @@ def test_dif(self) -> None: project_id=self.project.id, checksum=total_checksum ).get() + assert dif.file is not None assert dif.file.headers == {"Content-Type": "text/x-breakpad"} def test_assemble_from_files(self) -> None: @@ -203,6 +204,7 @@ def test_assemble_debug_id_override(self) -> None: project_id=self.project.id, checksum=total_checksum ).get() + assert dif.file is not None assert dif.file.headers == {"Content-Type": "text/x-breakpad"} assert dif.debug_id == "67e9247c-814e-392b-a027-dbde6748fcbf-beef"