views.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  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 django.core.files import File as DjangoFile
  8. from django.db import transaction
  9. from django.shortcuts import get_object_or_404
  10. from rest_framework import exceptions, status, views
  11. from rest_framework.response import Response
  12. from symbolic import ProguardMapper
  13. from apps.files.models import File, FileBlob
  14. from apps.organizations_ext.models import Organization
  15. from apps.projects.models import Project
  16. from .models import DebugInformationFile
  17. from .permissions import (
  18. DifsAssemblePermission,
  19. DymsPermission,
  20. ProjectReprocessingPermission,
  21. )
  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. class DifsAssembleAPIView(views.APIView):
  25. permission_classes = [DifsAssemblePermission]
  26. def post(self, request, organization_slug, project_slug):
  27. organization = get_object_or_404(
  28. Organization, slug=organization_slug.lower(), users=self.request.user
  29. )
  30. self.check_object_permissions(request, organization)
  31. project = get_object_or_404(Project, slug=project_slug.lower())
  32. if project.organization.id != organization.id:
  33. raise exceptions.PermissionDenied(
  34. "The project is not under this organization"
  35. )
  36. responses = {}
  37. files = request.data.items()
  38. for checksum, file in files:
  39. chunks = file.get("chunks", [])
  40. name = file.get("name", None)
  41. debug_id = file.get("debug_id", None)
  42. file = (
  43. DebugInformationFile.objects.filter(
  44. project__slug=project_slug, file__checksum=checksum
  45. )
  46. .select_related("file")
  47. .first()
  48. )
  49. if file is not None:
  50. responses[checksum] = {
  51. "state": DIF_STATE_OK,
  52. "missingChunks": [],
  53. }
  54. continue
  55. existed_chunks = FileBlob.objects.filter(checksum__in=chunks).values_list(
  56. "checksum", flat=True
  57. )
  58. missing_chunks = list(set(chunks) - set(existed_chunks))
  59. if len(missing_chunks) != 0:
  60. responses[checksum] = {
  61. "state": DIF_STATE_NOT_FOUND,
  62. "missingChunks": missing_chunks,
  63. }
  64. continue
  65. responses[checksum] = {"state": DIF_STATE_CREATED, "missingChunks": []}
  66. difs_assemble.delay(project_slug, name, checksum, chunks, debug_id)
  67. return Response(responses)
  68. class ProjectReprocessingAPIView(views.APIView):
  69. """
  70. Not implemented. It is a dummy API to keep `sentry-cli upload-dif` happy
  71. """
  72. permission_classes = [ProjectReprocessingPermission]
  73. def post(self, request, organization_slug, project_slug):
  74. return Response()
  75. def extract_proguard_id(name):
  76. match = re.search("proguard/([-a-fA-F0-9]+).txt", name)
  77. if match is None:
  78. return
  79. return match.group(1)
  80. def extract_proguard_metadata(proguard_file):
  81. try:
  82. mapper = ProguardMapper.open(proguard_file)
  83. if mapper is None:
  84. return
  85. metadata = {"arch": "any", "feature": "mapping"}
  86. return metadata
  87. except Exception:
  88. pass
  89. class DsymsAPIView(views.APIView):
  90. """
  91. Implementation of /files/dsyms API View
  92. """
  93. permission_classes = [DymsPermission]
  94. def post(self, request, organization_slug, project_slug):
  95. organization = get_object_or_404(
  96. Organization, slug=organization_slug.lower(), users=self.request.user
  97. )
  98. self.check_object_permissions(request, organization)
  99. project = get_object_or_404(Project, slug=project_slug.lower())
  100. if project.organization.id != organization.id:
  101. raise exceptions.PermissionDenied(
  102. "The project is not under this organization"
  103. )
  104. if "file" not in request.data:
  105. return Response(
  106. {"error": "No file uploaded"},
  107. status=status.HTTP_400_BAD_REQUEST,
  108. )
  109. try:
  110. file = request.data["file"]
  111. if file.size > MAX_UPLOAD_BLOB_SIZE:
  112. return Response(
  113. {"error": "File size too large"},
  114. status=status.HTTP_400_BAD_REQUEST,
  115. )
  116. content = file.read()
  117. buffer = io.BytesIO(content)
  118. if zipfile.is_zipfile(buffer) is False:
  119. return Response(
  120. {"error": "Invalid file type uploaded"},
  121. status=status.HTTP_400_BAD_REQUEST,
  122. )
  123. results = []
  124. with zipfile.ZipFile(buffer) as uploaded_zip_file:
  125. for filename in uploaded_zip_file.namelist():
  126. proguard_id = extract_proguard_id(filename)
  127. if proguard_id is None:
  128. return Response(
  129. {"error": "Invalid proguard mapping file uploaded"}, # noqa
  130. status=status.HTTP_400_BAD_REQUEST,
  131. )
  132. with uploaded_zip_file.open(filename) as proguard_file:
  133. result = self.create_dif_from_read_only_file(
  134. proguard_file, project, proguard_id, filename
  135. )
  136. if result is None:
  137. return Response(
  138. {"error": "Invalid proguard mapping file uploaded"}, # noqa
  139. status=status.HTTP_400_BAD_REQUEST,
  140. )
  141. results.append(result)
  142. return Response(results)
  143. except Exception as err:
  144. return Response(
  145. {"error": str(err)},
  146. status=status.HTTP_400_BAD_REQUEST,
  147. )
  148. def create_dif_from_read_only_file(
  149. self, proguard_file, project, proguard_id, filename
  150. ):
  151. with tempfile.NamedTemporaryFile("br+") as tmp:
  152. content = proguard_file.read()
  153. tmp.write(content)
  154. tmp.flush()
  155. metadata = extract_proguard_metadata(tmp.name)
  156. if metadata is None:
  157. return None
  158. checksum = sha1(content).hexdigest()
  159. with transaction.atomic():
  160. size = len(content)
  161. blob = FileBlob.objects.filter(checksum=checksum).first()
  162. if blob is None:
  163. blob = FileBlob(checksum=checksum, size=size) # noqa
  164. blob.blob.save(filename, DjangoFile(tmp))
  165. blob.save()
  166. fileobj = File.objects.filter(checksum=checksum).first()
  167. if fileobj is None:
  168. fileobj = File()
  169. fileobj.name = filename
  170. fileobj.headers = {}
  171. fileobj.checksum = checksum
  172. fileobj.size = size
  173. fileobj.blob = blob
  174. fileobj.save()
  175. dif = DebugInformationFile.objects.filter(
  176. file__checksum=checksum, project=project
  177. ).first()
  178. if dif is None:
  179. dif = DebugInformationFile()
  180. dif.name = filename
  181. dif.project = project
  182. dif.file = fileobj
  183. dif.data = {
  184. "arch": metadata["arch"],
  185. "debug_id": proguard_id,
  186. "symbol_type": "proguard",
  187. "features": ["mapping"],
  188. }
  189. dif.save()
  190. result = {
  191. "id": dif.id,
  192. "debugId": proguard_id,
  193. "cpuName": "any",
  194. "objectName": "proguard-mapping",
  195. "symbolType": "proguard",
  196. "size": size,
  197. "sha1": checksum,
  198. "data": {"features": ["mapping"]},
  199. "headers": {"Content-Type": "text/x-proguard+plain"},
  200. "dateCreated": fileobj.created,
  201. }
  202. return result