views.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  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 files.models import File, FileBlob
  14. from organizations_ext.models import Organization
  15. from 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. {
  139. "error": "Invalid proguard mapping file uploaded"
  140. }, # noqa
  141. status=status.HTTP_400_BAD_REQUEST,
  142. )
  143. results.append(result)
  144. return Response(results)
  145. except Exception as err:
  146. return Response(
  147. {"error": str(err)},
  148. status=status.HTTP_400_BAD_REQUEST,
  149. )
  150. def create_dif_from_read_only_file(
  151. self, proguard_file, project, proguard_id, filename
  152. ):
  153. with tempfile.NamedTemporaryFile("br+") as tmp:
  154. content = proguard_file.read()
  155. tmp.write(content)
  156. tmp.flush()
  157. metadata = extract_proguard_metadata(tmp.name)
  158. if metadata is None:
  159. return None
  160. checksum = sha1(content).hexdigest()
  161. with transaction.atomic():
  162. size = len(content)
  163. blob = FileBlob.objects.filter(checksum=checksum).first()
  164. if blob is None:
  165. blob = FileBlob(checksum=checksum, size=size) # noqa
  166. blob.blob.save(filename, DjangoFile(tmp))
  167. blob.save()
  168. fileobj = File.objects.filter(checksum=checksum).first()
  169. if fileobj is None:
  170. fileobj = File()
  171. fileobj.name = filename
  172. fileobj.headers = {}
  173. fileobj.checksum = checksum
  174. fileobj.size = size
  175. fileobj.blob = blob
  176. fileobj.save()
  177. dif = DebugInformationFile.objects.filter(
  178. file__checksum=checksum, project=project
  179. ).first()
  180. if dif is None:
  181. dif = DebugInformationFile()
  182. dif.name = filename
  183. dif.project = project
  184. dif.file = fileobj
  185. dif.data = {
  186. "arch": metadata["arch"],
  187. "debug_id": proguard_id,
  188. "symbol_type": "proguard",
  189. "features": ["mapping"],
  190. }
  191. dif.save()
  192. result = {
  193. "id": dif.id,
  194. "debugId": proguard_id,
  195. "cpuName": "any",
  196. "objectName": "proguard-mapping",
  197. "symbolType": "proguard",
  198. "size": size,
  199. "sha1": checksum,
  200. "data": {"features": ["mapping"]},
  201. "headers": {"Content-Type": "text/x-proguard+plain"},
  202. "dateCreated": fileobj.created,
  203. }
  204. return result