diff --git a/bugsink/urls.py b/bugsink/urls.py index 80a32f5..381d1b7 100644 --- a/bugsink/urls.py +++ b/bugsink/urls.py @@ -55,6 +55,7 @@ urlpatterns = [ path('teams/', include('teams.urls')), path('events/', include('events.urls')), path('issues/', include('issues.urls')), + path('files/', include('files.urls')), path('admin/', admin.site.urls), diff --git a/files/admin.py b/files/admin.py index b5163c4..198bdf5 100644 --- a/files/admin.py +++ b/files/admin.py @@ -1,4 +1,7 @@ from django.contrib import admin +from django.urls import reverse +from django.utils.html import format_html + from .models import Chunk, File, FileMetadata @@ -11,9 +14,15 @@ class ChunkAdmin(admin.ModelAdmin): @admin.register(File) class FileAdmin(admin.ModelAdmin): - list_display = ('filename', 'checksum', 'size') + list_display = ('filename', 'checksum', 'size', 'download_link') search_fields = ('checksum',) - readonly_fields = ('data',) + readonly_fields = ('data', 'download_link') + + def download_link(self, obj): + return format_html( + '{}', + reverse("download_file", args=(obj.checksum,)), str(obj.filename), + ) @admin.register(FileMetadata) diff --git a/files/urls.py b/files/urls.py new file mode 100644 index 0000000..29ac7fc --- /dev/null +++ b/files/urls.py @@ -0,0 +1,27 @@ +from django.urls import path +from django.urls import register_converter + +from .views import download_file + + +def regex_converter(passed_regex): + # copy/pasta w/ issues/urls.py + + class RegexConverter: + regex = passed_regex + + def to_python(self, value): + return value + + def to_url(self, value): + return value + + return RegexConverter + + +register_converter(regex_converter("[0-9a-f]{40}"), "sha1") + + +urlpatterns = [ + path('downloads//', download_file, name='download_file'), +] diff --git a/files/views.py b/files/views.py index 90e1dd2..fff8d4a 100644 --- a/files/views.py +++ b/files/views.py @@ -3,9 +3,11 @@ import json from hashlib import sha1 from gzip import GzipFile from io import BytesIO +from os.path import basename from django.http import JsonResponse, HttpResponse from django.views.decorators.csrf import csrf_exempt +from django.contrib.auth.decorators import user_passes_test from sentry.assemble import ChunkFileState @@ -154,7 +156,7 @@ def assemble_artifact_bundle(bundle_checksum, chunk_checksums): checksum = sha1(file_data).hexdigest() - filename = manifest_entry.get("url", filename)[:255] + filename = basename(manifest_entry.get("url", filename))[:255] file, _ = File.objects.get_or_create( checksum=checksum, @@ -226,3 +228,11 @@ def artifact_bundle_assemble(request, organization_slug): # NOTE: as it stands, we process the bundle inline, so arguably we could return "OK" here too; "CREATED" is what # sentry returns though, so for faithful mimicking it's the safest bet. return JsonResponse({"state": ChunkFileState.CREATED, "missingChunks": []}) + + +@user_passes_test(lambda u: u.is_superuser) +def download_file(request, checksum): + file = File.objects.get(checksum=checksum) + response = HttpResponse(file.data, content_type="application/octet-stream") + response["Content-Disposition"] = f"attachment; filename={file.filename}" + return response