api.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. """Port of sentry.api.endpoints.debug_files.DifAssembleEndpoint"""
  2. import io
  3. import re
  4. import tempfile
  5. import zipfile
  6. from hashlib import sha1
  7. from asgiref.sync import sync_to_async
  8. from django.core.files import File as DjangoFile
  9. from django.shortcuts import aget_object_or_404
  10. from ninja import File as NinjaFile
  11. from ninja import Router
  12. from ninja.errors import HttpError
  13. from ninja.files import UploadedFile
  14. from symbolic import ProguardMapper
  15. from apps.files.models import File, FileBlob
  16. from apps.organizations_ext.models import Organization
  17. from apps.projects.models import Project
  18. from glitchtip.api.authentication import AuthHttpRequest
  19. from glitchtip.utils import async_call_celery_task
  20. from .models import DebugInformationFile
  21. from .schema import AssemblePayload
  22. from .tasks import DIF_STATE_CREATED, DIF_STATE_NOT_FOUND, DIF_STATE_OK, difs_assemble
  23. MAX_UPLOAD_BLOB_SIZE = 32 * 1024 * 1024 # 32MB
  24. router = Router()
  25. @router.post(
  26. "projects/{slug:organization_slug}/{slug:project_slug}/files/difs/assemble/"
  27. )
  28. async def difs_assemble_api(
  29. request: AuthHttpRequest,
  30. organization_slug: str,
  31. project_slug: str,
  32. payload: AssemblePayload,
  33. ):
  34. organization = await aget_object_or_404(
  35. Organization, slug=organization_slug.lower(), users=request.auth.user_id
  36. )
  37. # self.check_object_permissions(request, organization)
  38. await aget_object_or_404(
  39. Project, slug=project_slug.lower(), organization=organization
  40. )
  41. responses = {}
  42. files = payload.root.items()
  43. for checksum, file in files:
  44. chunks = file.chunks
  45. name = file.name
  46. debug_id = file.debug_id
  47. debug_file = await (
  48. DebugInformationFile.objects.filter(
  49. project__slug=project_slug, file__checksum=checksum
  50. )
  51. .select_related("file")
  52. .afirst()
  53. )
  54. if debug_file is not None:
  55. responses[checksum] = {
  56. "state": DIF_STATE_OK,
  57. "missingChunks": [],
  58. }
  59. continue
  60. existed_chunks = [
  61. file_blob
  62. async for file_blob in FileBlob.objects.filter(
  63. checksum__in=chunks
  64. ).values_list("checksum", flat=True)
  65. ]
  66. missing_chunks = list(set(chunks) - set(existed_chunks))
  67. if len(missing_chunks) != 0:
  68. responses[checksum] = {
  69. "state": DIF_STATE_NOT_FOUND,
  70. "missingChunks": missing_chunks,
  71. }
  72. continue
  73. responses[checksum] = {"state": DIF_STATE_CREATED, "missingChunks": []}
  74. await async_call_celery_task(
  75. difs_assemble, project_slug, name, checksum, chunks, debug_id
  76. )
  77. return responses
  78. @router.post("projects/{slug:organization_slug}/{slug:project_slug}/reprocessing/")
  79. async def project_reprocessing(
  80. request: AuthHttpRequest,
  81. organization_slug: str,
  82. project_slug: str,
  83. ):
  84. """
  85. Not implemented. It is a dummy API to keep `sentry-cli upload-dif` happy
  86. """
  87. return None
  88. def extract_proguard_id(name: str):
  89. match = re.search("proguard/([-a-fA-F0-9]+).txt", name)
  90. if match is None:
  91. return
  92. return match.group(1)
  93. def extract_proguard_metadata(proguard_file):
  94. try:
  95. mapper = ProguardMapper.open(proguard_file)
  96. if mapper is None:
  97. return
  98. metadata = {"arch": "any", "feature": "mapping"}
  99. return metadata
  100. except Exception:
  101. pass
  102. async def create_dif_from_read_only_file(proguard_file, project, proguard_id, filename):
  103. with tempfile.NamedTemporaryFile("br+") as tmp:
  104. content = proguard_file.read()
  105. tmp.write(content)
  106. tmp.flush()
  107. metadata = extract_proguard_metadata(tmp.name)
  108. if metadata is None:
  109. return None
  110. checksum = sha1(content).hexdigest()
  111. size = len(content)
  112. blob = await FileBlob.objects.filter(checksum=checksum).afirst()
  113. if blob is None:
  114. blob = FileBlob(checksum=checksum, size=size) # noqa
  115. await sync_to_async(blob.blob.save)(filename, DjangoFile(tmp))
  116. await blob.asave()
  117. fileobj = await File.objects.filter(checksum=checksum).afirst()
  118. if fileobj is None:
  119. fileobj = File()
  120. fileobj.name = filename
  121. fileobj.headers = {}
  122. fileobj.checksum = checksum
  123. fileobj.size = size
  124. fileobj.blob = blob
  125. await fileobj.asave()
  126. dif = await DebugInformationFile.objects.filter(
  127. file__checksum=checksum, project=project
  128. ).afirst()
  129. if dif is None:
  130. dif = DebugInformationFile()
  131. dif.name = filename
  132. dif.project = project
  133. dif.file = fileobj
  134. dif.data = {
  135. "arch": metadata["arch"],
  136. "debug_id": proguard_id,
  137. "symbol_type": "proguard",
  138. "features": ["mapping"],
  139. }
  140. await dif.asave()
  141. result = {
  142. "id": dif.id,
  143. "debugId": proguard_id,
  144. "cpuName": "any",
  145. "objectName": "proguard-mapping",
  146. "symbolType": "proguard",
  147. "size": size,
  148. "sha1": checksum,
  149. "data": {"features": ["mapping"]},
  150. "headers": {"Content-Type": "text/x-proguard+plain"},
  151. "dateCreated": fileobj.created,
  152. }
  153. return result
  154. @router.post("projects/{slug:organization_slug}/{slug:project_slug}/files/dsyms/")
  155. async def dsyms(
  156. request: AuthHttpRequest,
  157. organization_slug: str,
  158. project_slug: str,
  159. file: UploadedFile = NinjaFile(...),
  160. ):
  161. organization = await aget_object_or_404(
  162. Organization, slug=organization_slug.lower(), users=request.auth.user_id
  163. )
  164. # self.check_object_permissions(request, organization)
  165. project = await aget_object_or_404(
  166. Project, slug=project_slug.lower(), organization=organization
  167. )
  168. if file.size > MAX_UPLOAD_BLOB_SIZE:
  169. raise HttpError(
  170. 400,
  171. "File size too large",
  172. )
  173. content = file.read()
  174. buffer = io.BytesIO(content)
  175. if zipfile.is_zipfile(buffer) is False:
  176. raise HttpError(400, "Invalid file type uploaded")
  177. results = []
  178. with zipfile.ZipFile(buffer) as uploaded_zip_file:
  179. for filename in uploaded_zip_file.namelist():
  180. proguard_id = extract_proguard_id(filename)
  181. if proguard_id is None:
  182. raise HttpError(400, "")
  183. with uploaded_zip_file.open(filename) as proguard_file:
  184. result = await create_dif_from_read_only_file(
  185. proguard_file, project, proguard_id, filename
  186. )
  187. if result is None:
  188. raise HttpError(
  189. 400,
  190. "Invalid proguard mapping file uploaded",
  191. )
  192. results.append(result)
  193. return results