def serve_document_from_s3(document, request): """ Download document from S3. This is to avoid reading the whole document by the Wagtail view and potentially risking DoS attack and the server timing out. """ # Skip this hook if not using django-storages boto3 backend. if not issubclass(get_storage_class(), S3Boto3Storage): return # Send document_served signal, same as Wagtail does. # https://github.com/wagtail/wagtail/blob/7938e81ab48327a084ac1dced9474c998fd44c2d/wagtail/documents/views/serve.py#L32-L33 document_served.send(sender=get_document_model(), instance=document, request=request) # Service file directly from S3. file_url = document.file.url # Generate redirect response and add never_cache headers. # Delete all existing headers. response = redirect(file_url) del response["Cache-control"] add_never_cache_headers(response) return response
def serve(request, document_id, document_filename): Document = get_document_model() doc = get_object_or_404(Document, id=document_id) # We want to ensure that the document filename provided in the URL matches the one associated with the considered # document_id. If not we can't be sure that the document the user wants to access is the one corresponding to the # <document_id, document_filename> pair. if doc.filename != document_filename: raise Http404('This document does not match the given filename.') for fn in hooks.get_hooks('before_serve_document'): result = fn(doc, request) if isinstance(result, HttpResponse): return result # Send document_served signal document_served.send(sender=Document, instance=doc, request=request) try: local_path = doc.file.path except NotImplementedError: local_path = None if local_path: # Use wagtail.utils.sendfile to serve the file; # this provides support for mimetypes, if-modified-since and django-sendfile backends if hasattr(settings, 'SENDFILE_BACKEND'): return sendfile(request, local_path, attachment=True, attachment_filename=doc.filename) else: # Fallback to streaming backend if user hasn't specified SENDFILE_BACKEND return sendfile(request, local_path, attachment=True, attachment_filename=doc.filename, backend=sendfile_streaming_backend.sendfile) else: # We are using a storage backend which does not expose filesystem paths # (e.g. storages.backends.s3boto.S3BotoStorage). # Fall back on pre-sendfile behaviour of reading the file content and serving it # as a StreamingHttpResponse wrapper = FileWrapper(doc.file) response = StreamingHttpResponse( wrapper, content_type='application/octet-stream') response[ 'Content-Disposition'] = 'attachment; filename=%s' % doc.filename # FIXME: storage backends are not guaranteed to implement 'size' response['Content-Length'] = doc.file.size return response
def serve(request, document_id, document_filename): Document = get_document_model() doc = get_object_or_404(Document, id=document_id) # We want to ensure that the document filename provided in the URL matches the one associated with the considered # document_id. If not we can't be sure that the document the user wants to access is the one corresponding to the # <document_id, document_filename> pair. if doc.filename != document_filename: raise Http404('This document does not match the given filename.') for fn in hooks.get_hooks('before_serve_document'): result = fn(doc, request) if isinstance(result, HttpResponse): return result # Send document_served signal document_served.send(sender=Document, instance=doc, request=request) try: local_path = doc.file.path except NotImplementedError: local_path = None if local_path: # Use wagtail.utils.sendfile to serve the file; # this provides support for mimetypes, if-modified-since and django-sendfile backends if hasattr(settings, 'SENDFILE_BACKEND'): return sendfile(request, local_path, attachment=True, attachment_filename=doc.filename) else: # Fallback to streaming backend if user hasn't specified SENDFILE_BACKEND return sendfile( request, local_path, attachment=True, attachment_filename=doc.filename, backend=sendfile_streaming_backend.sendfile ) else: # We are using a storage backend which does not expose filesystem paths # (e.g. storages.backends.s3boto.S3BotoStorage). # Fall back on pre-sendfile behaviour of reading the file content and serving it # as a StreamingHttpResponse wrapper = FileWrapper(doc.file) response = StreamingHttpResponse(wrapper, content_type='application/octet-stream') response['Content-Disposition'] = 'attachment; filename=%s' % doc.filename # FIXME: storage backends are not guaranteed to implement 'size' response['Content-Length'] = doc.file.size return response
def serve_document_from_s3(document, request): # Skip this hook if not using django-storages boto3 backend. if not issubclass(get_storage_class(), S3Boto3Storage): return # Send document_served signal. document_served.send(sender=get_document_model(), instance=document, request=request) # Get direct S3 link. file_url = document.file.url # Generate redirect response and add never_cache headers. response = redirect(file_url) del response['Cache-control'] add_never_cache_headers(response) return response
def serve(request, document_id, document_filename): Document = get_document_model() doc = get_object_or_404(Document, id=document_id) # We want to ensure that the document filename provided in the URL matches the one associated with the considered # document_id. If not we can't be sure that the document the user wants to access is the one corresponding to the # <document_id, document_filename> pair. if doc.filename != document_filename: raise Http404('This document does not match the given filename.') for fn in hooks.get_hooks('before_serve_document'): result = fn(doc, request) if isinstance(result, HttpResponse): return result # Send document_served signal document_served.send(sender=Document, instance=doc, request=request) try: local_path = doc.file.path except NotImplementedError: local_path = None try: direct_url = doc.file.url except NotImplementedError: direct_url = None serve_method = getattr(settings, 'WAGTAILDOCS_SERVE_METHOD', None) # If no serve method has been specified, select an appropriate default for the storage backend: # redirect for remote storages (i.e. ones that provide a url but not a local path) and # serve_view for all other cases if serve_method is None: if direct_url and not local_path: serve_method = 'redirect' else: serve_method = 'serve_view' if serve_method in ('redirect', 'direct') and direct_url: # Serve the file by redirecting to the URL provided by the underlying storage; # this saves the cost of delivering the file via Python. # For serve_method == 'direct', this view should not normally be reached # (the document URL as used in links should point directly to the storage URL instead) # but we handle it as a redirect to provide sensible fallback / # backwards compatibility behaviour. return redirect(direct_url) if local_path: # Use wagtail.utils.sendfile to serve the file; # this provides support for mimetypes, if-modified-since and django-sendfile backends if hasattr(settings, 'SENDFILE_BACKEND'): return sendfile(request, local_path, attachment=True, attachment_filename=doc.filename) else: # Fallback to streaming backend if user hasn't specified SENDFILE_BACKEND return sendfile(request, local_path, attachment=True, attachment_filename=doc.filename, backend=sendfile_streaming_backend.sendfile) else: # We are using a storage backend which does not expose filesystem paths # (e.g. storages.backends.s3boto.S3BotoStorage) AND the developer has not allowed # redirecting to the file url directly. # Fall back on pre-sendfile behaviour of reading the file content and serving it # as a StreamingHttpResponse wrapper = FileWrapper(doc.file) response = StreamingHttpResponse( wrapper, content_type='application/octet-stream') response[ 'Content-Disposition'] = 'attachment; filename=%s' % doc.filename # FIXME: storage backends are not guaranteed to implement 'size' response['Content-Length'] = doc.file.size return response
def serve(request, document_id, document_filename): Document = get_document_model() doc = get_object_or_404(Document, id=document_id) # We want to ensure that the document filename provided in the URL matches the one associated with the considered # document_id. If not we can't be sure that the document the user wants to access is the one corresponding to the # <document_id, document_filename> pair. if doc.filename != document_filename: raise Http404("This document does not match the given filename.") for fn in hooks.get_hooks("before_serve_document"): result = fn(doc, request) if isinstance(result, HttpResponse): return result # Send document_served signal document_served.send(sender=Document, instance=doc, request=request) try: local_path = doc.file.path except NotImplementedError: local_path = None try: direct_url = doc.file.url except NotImplementedError: direct_url = None serve_method = getattr(settings, "WAGTAILDOCS_SERVE_METHOD", None) # If no serve method has been specified, select an appropriate default for the storage backend: # redirect for remote storages (i.e. ones that provide a url but not a local path) and # serve_view for all other cases if serve_method is None: if direct_url and not local_path: serve_method = "redirect" else: serve_method = "serve_view" if serve_method in ("redirect", "direct") and direct_url: # Serve the file by redirecting to the URL provided by the underlying storage; # this saves the cost of delivering the file via Python. # For serve_method == 'direct', this view should not normally be reached # (the document URL as used in links should point directly to the storage URL instead) # but we handle it as a redirect to provide sensible fallback / # backwards compatibility behaviour. return redirect(direct_url) if local_path: # Use wagtail.utils.sendfile to serve the file; # this provides support for mimetypes, if-modified-since and django-sendfile backends sendfile_opts = { "attachment": (doc.content_disposition != "inline"), "attachment_filename": doc.filename, "mimetype": doc.content_type, } if not hasattr(settings, "SENDFILE_BACKEND"): # Fallback to streaming backend if user hasn't specified SENDFILE_BACKEND sendfile_opts["backend"] = sendfile_streaming_backend.sendfile return sendfile(request, local_path, **sendfile_opts) else: # We are using a storage backend which does not expose filesystem paths # (e.g. storages.backends.s3boto.S3BotoStorage) AND the developer has not allowed # redirecting to the file url directly. # Fall back on pre-sendfile behaviour of reading the file content and serving it # as a StreamingHttpResponse wrapper = FileWrapper(doc.file) response = StreamingHttpResponse(wrapper, doc.content_type) # set filename and filename* to handle non-ascii characters in filename # see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition response["Content-Disposition"] = doc.content_disposition # FIXME: storage backends are not guaranteed to implement 'size' response["Content-Length"] = doc.file.size return response
def serve_wagtail_doc(request, document_id, document_filename): """ Replacement for `wagtail.documents.views.serve` Substantial copying from original method: https://github.com/wagtail/wagtail/blob/master/wagtail/documents/views/serve.py Wagtail's default document view serves everything as an attachment. We'll bounce back to the URL and let the media server serve it. """ # these first two passages copied from wagtail/documents/views/serve.py Document = get_document_model() doc = get_object_or_404(Document, id=document_id) # We want to ensure that the document filename provided in the URL matches # the one associated with the considered document_id. If not we can't be # sure that the document the user wants to access is the one corresponding # to the <document_id, document_filename> pair. if doc.filename != document_filename: raise Http404('This document does not match the given filename.') for fn in hooks.get_hooks('before_serve_document'): result = fn(doc, request) if isinstance(result, HttpResponse): return result # Send document_served signal & log document_served.send(sender=Document, instance=doc, request=request) # ensure log output is valid CSV output = io.StringIO() writer = csv.writer(output, quoting=csv.QUOTE_NONNUMERIC) writer.writerow( [document_id, document_filename, request.headers['User-Agent']]) logger.info(output.getvalue().strip()) try: local_path = doc.file.path except NotImplementedError: local_path = None try: direct_url = doc.file.url except NotImplementedError: direct_url = None serve_method = getattr(settings, 'WAGTAILDOCS_SERVE_METHOD', None) # If no serve method has been specified, select an appropriate default for the storage backend: # redirect for remote storages (i.e. ones that provide a url but not a local path) and # serve_view for all other cases if serve_method is None: if direct_url and not local_path: serve_method = 'redirect' else: serve_method = 'serve_view' if serve_method in ('redirect', 'direct') and direct_url: # Serve the file by redirecting to the URL provided by the underlying storage; # this saves the cost of delivering the file via Python. # For serve_method == 'direct', this view should not normally be reached # (the document URL as used in links should point directly to the storage URL instead) # but we handle it as a redirect to provide sensible fallback / # backwards compatibility behaviour. return redirect(direct_url) if local_path: # Use wagtail.utils.sendfile to serve the file; # this provides support for mimetypes, if-modified-since and django-sendfile backends if hasattr(settings, 'SENDFILE_BACKEND'): return sendfile(request, local_path, attachment=False, attachment_filename=doc.filename) else: # Fallback to streaming backend if user hasn't specified SENDFILE_BACKEND return sendfile(request, local_path, attachment=False, attachment_filename=doc.filename, backend=sendfile_streaming_backend.sendfile) else: # We are using a storage backend which does not expose filesystem paths # (e.g. storages.backends.s3boto.S3BotoStorage) AND the developer has not allowed # redirecting to the file url directly. # Fall back on pre-sendfile behaviour of reading the file content and serving it # as a StreamingHttpResponse wrapper = FileWrapper(doc.file) response = StreamingHttpResponse( wrapper, content_type='application/octet-stream') # FIXME: storage backends are not guaranteed to implement 'size' response['Content-Length'] = doc.file.size return response