views.py 9.0 KB

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