From 52c2cd03a1612a477d91bbb23ea5ea0b7fef801b Mon Sep 17 00:00:00 2001 From: GraceDB <gracedb@gracedb-dev2.ligo.uwm.edu> Date: Fri, 13 Jul 2018 08:22:23 -0500 Subject: [PATCH] Update to event file serving Updated event static file serving to use X-Sendfile with Apache. This is faster, more efficient, and seems to resolve the issue with FITS file downloads from the web browser: https://bugs.ligo.org/redmine/issues/6194 --- gracedb/core/http.py | 5 + gracedb/events/api/urls.py | 4 +- gracedb/events/api/views.py | 95 ++----------------- gracedb/events/buildVOEvent.py | 4 +- gracedb/events/models.py | 6 +- gracedb/events/urls.py | 5 +- gracedb/events/view_logic.py | 2 +- gracedb/events/views.py | 19 +++- .../templates/gracedb/event_detail_script.js | 9 +- gracedb/templates/gracedb/event_filelist.html | 2 +- 10 files changed, 48 insertions(+), 103 deletions(-) diff --git a/gracedb/core/http.py b/gracedb/core/http.py index 1b464e09a..5ba5a034c 100644 --- a/gracedb/core/http.py +++ b/gracedb/core/http.py @@ -37,6 +37,11 @@ def serve_file(file_path, ResponseClass=HttpResponse): response.content_type = content_type response['Content-Type'] = content_type + # Set encoding (again in both places) + if encoding is not None: + response.encoding = encoding + response['Content-Encoding'] = encoding + # For binary files, add the file as an attachment (direct download instead # of opening in browser window) if content_type == "application/octet-stream": diff --git a/gracedb/events/api/urls.py b/gracedb/events/api/urls.py index 016817ccf..6a46cdb6d 100644 --- a/gracedb/events/api/urls.py +++ b/gracedb/events/api/urls.py @@ -89,6 +89,6 @@ urlpatterns = [ url(r'^performance/$', PerformanceInfo.as_view(), name='performance-info'), # Legacy - url(r'^event/(?P<graceid>\w[\d]+)/files/(?P<filename>.+)?$', download, - name="download2"), + url(r'^event/(?P<graceid>\w[\d]+)/files/(?P<filename>.+)?$', + Files.as_view(), name="file-download"), ] diff --git a/gracedb/events/api/views.py b/gracedb/events/api/views.py index 033d4855b..b917ede48 100644 --- a/gracedb/events/api/views.py +++ b/gracedb/events/api/views.py @@ -31,6 +31,7 @@ from ..permission_utils import user_has_perm, filter_events_for_user, \ from .throttles import EventCreationThrottle, AnnotationThrottle from core.vfile import VersionedFile +from core.http import check_and_serve_file from guardian.models import GroupObjectPermission @@ -1570,72 +1571,6 @@ class GracedbRoot(APIView): "voevent-types" : dict(VOEvent.VOEVENT_TYPE_CHOICES), }) -################################################################## -# Old. Must support this. -def download(request, graceid, filename=""): - # Do not filename to be None. That messes up later os.path.join - filename = filename or "" - - try: - event = Event.getByGraceid(graceid) - if not user_has_perm(request.user, 'view', event): - return HttpResponseForbidden("Forbidden") - except Event.DoesNotExist: - return HttpResponseNotFound("Event not found") - - # If the user is external, check for authorization - if is_external(request.user): - if not check_external_file_access(event, filename): - msg = "You do not have permission to view this file." - return HttpResponseForbidden(msg) - - filepath = os.path.join(event.datadir(), filename) - - if not os.path.exists(filepath): - response = HttpResponseNotFound("File does not exist") - elif not os.access(filepath, os.R_OK): - response = HttpResponseNotFound("File not readable") - elif os.path.isfile(filepath): - # get an actual file. - content_type, encoding = VersionedFile.guess_mimetype(filepath) - content_type = content_type or "application/octet-stream" - # XXX encoding should probably not be ignored. - - # Get a pretty filename. Just strip off version info. - # XXX This will break if you change the file-version naming convention - # (by, for instance, using a different delimiter than a comma). - try: - ind = filename.index(',') - pretty_filename = filename[:ind] - except ValueError: - pretty_filename = filename - - response = HttpResponse(open(filepath, "r"), content_type=content_type) - if content_type == "application/octet-stream": - #response['Content-Disposition'] = 'attachment; filename=%s' % os.path.basename(filename) - response['Content-Disposition'] = 'attachment; filename=%s' % pretty_filename - else: - response['Content-Disposition'] = 'inline; filename=%s' % pretty_filename - if encoding is not None: - response['Content-Encoding'] = encoding - elif not filename: - # Get list of files w/urls. - rv = {} - filepath = event.datadir() - for dirname, dirnames, filenames in os.walk(filepath): - dirname = dirname[len(filepath):] # cut off base event dir path - for filename in filenames: - # relative path from root of event data dir - filename = os.path.join(dirname, filename) - rv[filename] = django_reverse(download, args=[graceid, filename]) - - response = HttpResponse(json.dumps(rv), content_type="application/json") - elif os.path.isdir(filepath): - response = HttpResponseForbidden("%s is a directory" % filename) - else: - response = HttpResponseServerError("Should not happen.") - - return response class Files(APIView): """Files Resource""" @@ -1653,28 +1588,17 @@ class Files(APIView): filepath = os.path.join(event.datadir(), filename) - if not os.path.exists(filepath): - response = HttpResponseNotFound("File does not exist") - elif not os.access(filepath, os.R_OK): - response = HttpResponseNotFound("File not readable") - elif os.path.isfile(filepath): - # get an actual file. - # If the user is external, check for authorization + # Check permissions for external users + if filename and os.path.isdir(filepath): + # XXX Really? + response = HttpResponseForbidden("%s is a directory" % filename) + elif filename: if is_external(request.user): if not check_external_file_access(event, filename): msg = "You do not have permission to view this file." return HttpResponseForbidden(msg) - - content_type, encoding = VersionedFile.guess_mimetype(filepath) - content_type = content_type or "application/octet-stream" - # XXX encoding should probably not be ignored. - response = HttpResponse(open(filepath, "r"), content_type=content_type) - if content_type == "application/octet-stream": - # Double quotes are required to avoid MUTLIPLE_CONTENT_DISPOSITION error - # in Chrome when the filename contains a comma. - response['Content-Disposition'] = 'attachment; filename="%s"' % os.path.basename(filename) - if encoding is not None: - response['Content-Encoding'] = encoding + response = check_and_serve_file(request, filepath, + ResponseClass=Response) elif not filename: # Get list of files w/urls. rv = {} @@ -1714,9 +1638,6 @@ class Files(APIView): request=request), }) response = Response(rv) - elif os.path.isdir(filepath): - # XXX Really? - response = HttpResponseForbidden("%s is a directory" % filename) else: response = HttpResponseServerError("Should not happen.") diff --git a/gracedb/events/buildVOEvent.py b/gracedb/events/buildVOEvent.py index 918da4144..66960ffab 100644 --- a/gracedb/events/buildVOEvent.py +++ b/gracedb/events/buildVOEvent.py @@ -291,9 +291,9 @@ def buildVOEvent(event, serial_number, voevent_type, request=None, skymap_filena g = Group('GW_SKYMAP', skymap_type) # shib urls. - shib_fits_skymap_url = get_url(request, objid, "file", fits_name) + shib_fits_skymap_url = get_url(request, objid, "file-download", fits_name) if img_name: - shib_png_skymap_url = get_url(request, objid, "file", img_name) + shib_png_skymap_url = get_url(request, objid, "file-download", img_name) # x509 urls. Hafta specify the api namespace. x509_fits_skymap_url = get_url(request, objid, "x509:files", fits_name) diff --git a/gracedb/events/models.py b/gracedb/events/models.py index c620faab5..c0b12f6cf 100644 --- a/gracedb/events/models.py +++ b/gracedb/events/models.py @@ -436,7 +436,8 @@ class EventLog(AutoIncrementModel): actual_filename = self.filename if self.file_version >= 0: actual_filename += ',%d' % self.file_version - return reverse('file', args=[self.event.graceid(), actual_filename]) + return reverse('file-download', args=[self.event.graceid(), + actual_filename]) else: return None @@ -1143,7 +1144,8 @@ class VOEvent(models.Model): actual_filename = self.filename if self.file_version >= 0: actual_filename += ',%d' % self.file_version - return reverse('file', args=[self.event.graceid(), actual_filename]) + return reverse('file-download', args=[self.event.graceid(), + actual_filename]) else: return None diff --git a/gracedb/events/urls.py b/gracedb/events/urls.py index 4825c55f4..b27534f08 100644 --- a/gracedb/events/urls.py +++ b/gracedb/events/urls.py @@ -6,7 +6,6 @@ from django.conf.urls import url from . import views #import django.views.generic.list_detail -from .api.views import download urlpatterns = [ url(r'^$', views.index, name="home-events"), @@ -26,8 +25,8 @@ urlpatterns = [ name="modify_signoff"), url(r'^(?P<graceid>[GEHMT]\d+)/files/$', views.file_list, name="file_list"), - url(r'^(?P<graceid>[GEHMT]\d+)/files/(?P<filename>.*)$', download, - name="file"), + url(r'^(?P<graceid>[GEHMT]\d+)/files/(?P<filename>.*)$', + views.file_download, name="file-download"), url(r'^(?P<graceid>[GEHMT]\d+)/log/(?P<num>([\d]*|preview))$', views.logentry, name="logentry"), url(r'^(?P<graceid>[GEHMT]\d+)/embblog/(?P<num>([\d]*|preview))$', diff --git a/gracedb/events/view_logic.py b/gracedb/events/view_logic.py index 945686c01..2634c3fd0 100644 --- a/gracedb/events/view_logic.py +++ b/gracedb/events/view_logic.py @@ -153,7 +153,7 @@ def _createEventFromForm(request, form): # XXX This reverse will give the web-interface URL, not the REST URL. # This could be a problem if anybody ever tries to use it. issueAlert(event, - request.build_absolute_uri(reverse("file", args=[event.graceid(),f.name])), + request.build_absolute_uri(reverse("file-download", args=[event.graceid(),f.name])), request.build_absolute_uri(reverse("view", args=[event.graceid()])), eventToDict(event, request=request)) except Exception, e: diff --git a/gracedb/events/views.py b/gracedb/events/views.py index f41a64a66..693056599 100644 --- a/gracedb/events/views.py +++ b/gracedb/events/views.py @@ -6,6 +6,7 @@ from django.template import RequestContext from django.urls import reverse from django.shortcuts import render +from core.http import check_and_serve_file from .models import Event, Group, EventLog, Label, Tag, Pipeline, Search, GrbEvent from .models import EMGroup, Signoff from .forms import CreateEventForm, EventSearchForm, SimpleSearchForm, SignoffForm @@ -14,7 +15,8 @@ from django.contrib.auth.models import User, Permission from django.contrib.auth.models import Group as AuthGroup from django.contrib.contenttypes.models import ContentType from .permission_utils import filter_events_for_user, user_has_perm -from .permission_utils import internal_user_required, is_external +from .permission_utils import internal_user_required, is_external, \ + check_external_file_access from guardian.models import GroupObjectPermission from .view_logic import _createEventFromForm @@ -866,6 +868,21 @@ def file_list(request, event): return render(request, 'gracedb/event_filelist.html', context=context) + +@event_and_auth_required +def file_download(request, event, filename): + + # If the user is external, check for authorization + if is_external(request.user): + if not check_external_file_access(event, filename): + msg = "You do not have permission to view this file." + return HttpResponseForbidden(msg) + + file_path = os.path.join(event.datadir, filename) + return check_and_serve_file(request, file_path, + ResponseClass=HttpResponse) + + # A view to modify the GroupObjectPermissions for an event. # This is very non-RESTful. If the action is 'expose', you # give the group both view and change permissions on the event. diff --git a/gracedb/templates/gracedb/event_detail_script.js b/gracedb/templates/gracedb/event_detail_script.js index 1c05477f8..879cb5c45 100644 --- a/gracedb/templates/gracedb/event_detail_script.js +++ b/gracedb/templates/gracedb/event_detail_script.js @@ -161,7 +161,8 @@ var eventLogListUrl = '{% url "shib:eventlog-list" object.graceid %}'; var eventLogSaveUrl = '{% url "logentry" object.graceid "" %}'; var embbEventLogListUrl = '{% url "shib:embbeventlog-list" object.graceid %}'; var emObservationListUrl = '{% url "shib:emobservation-list" object.graceid %}'; -var skymapJsonUrl = '{% url "file" object.graceid "" %}'; +var fileDownloadUrl = '{% url "file-download" object.graceid "FAKE_FILE_NAME" %}'; +var skymapJsonUrl = '{% url "shib:files" object.graceid "" %}'; var skymapViewerUrl = '{{ SKYMAP_VIEWER_SERVICE_URL }}'; // This little list determines the priority ordering of the digest sections. @@ -636,7 +637,7 @@ require([ // Putting this in the innerHTML allows users to create comments in HTML. // Whereas, inserting the comment with the put selector escapes it. commentDiv.innerHTML += value + ' '; - if (object.filename) put(commentDiv, 'a[href=$]', object.file, object.filename); + if (object.filename) put(commentDiv, 'a[href=$]', fileDownloadUrl.replace("FAKE_FILE_NAME", object.filename + "," + object.file_version), object.filename); // Branson, 3/3/15 //if (object.filename == 'skymap.json') { var isItJson = object.filename.indexOf(".json"); @@ -796,7 +797,7 @@ require([ // Putting this in the innerHTML allows users to create comments in HTML. // Whereas, inserting the comment with the put selector escapes it. commentDiv.innerHTML += value + ' '; - if (object.filename) put(commentDiv, 'a[href=$]', object.file, object.filename); + if (object.filename) put(commentDiv, 'a[href=$]', fileDownloadUrl.replace("FAKE_FILE_NAME", object.filename + "," + object.file_version), object.filename); // Create tag-related features var tagButtonContainer = put(commentDiv, 'div.tagButtonContainerClass'); // For each existing tag on a log message, we will make a little widget @@ -888,7 +889,7 @@ require([ // Putting this in the innerHTML allows users to create comments in HTML. // Whereas, inserting the comment with the put selector escapes it. commentDiv.innerHTML += value + ' '; - if (object.filename) put(commentDiv, 'a[href=$]', object.file, object.filename); + if (object.filename) put(commentDiv, 'a[href=$]', fileDownloadUrl.replace("FAKE_FILE_NAME", object.filename + "," + object.file_version), object.filename); // Create tag-related features var tagButtonContainer = put(commentDiv, 'div.tagButtonContainerClass'); // For each existing tag on a log message, we will make a little widget diff --git a/gracedb/templates/gracedb/event_filelist.html b/gracedb/templates/gracedb/event_filelist.html index 1f413294a..b6277067e 100644 --- a/gracedb/templates/gracedb/event_filelist.html +++ b/gracedb/templates/gracedb/event_filelist.html @@ -8,7 +8,7 @@ <ul> {% for filename in file_list %} -<li><a href="{% url "file" graceid filename %}">{{ filename }}</a></li> +<li><a href="{% url "file-download" graceid filename %}">{{ filename }}</a></li> {% endfor %} </ul> -- GitLab