diff --git a/opentech/apply/funds/urls.py b/opentech/apply/funds/urls.py
index dd75bcd4a84bc64ab50dff8c52f85ed9ae0a0155..386993422d70d095bd1c10dc336fd9c0616fdc74 100644
--- a/opentech/apply/funds/urls.py
+++ b/opentech/apply/funds/urls.py
@@ -12,7 +12,7 @@ from .views import (
     SubmissionOverviewView,
     SubmissionSealedView,
     SubmissionDeleteView,
-    SubmissionPrivateMediaRedirectView,
+    SubmissionPrivateMediaView,
 )
 from .api_views import (
     CommentEdit,
@@ -39,7 +39,7 @@ submission_urls = ([
     path('all/', SubmissionListView.as_view(), name="list"),
     path(
         'documents/submission/<int:submission_id>/<uuid:field_id>/<str:file_name>/',
-        SubmissionPrivateMediaRedirectView.as_view(), name='private_media_redirect'
+        SubmissionPrivateMediaView.as_view(), name='serve_private_media'
     ),
     path('<int:pk>/', include([
         path('', SubmissionDetailView.as_view(), name="detail"),
diff --git a/opentech/apply/funds/views.py b/opentech/apply/funds/views.py
index 135988c9cc3ec3bb77221f58b9d21b016a276c01..a5de558a1c6bf183312f2fc79f8bbc1b6b91bd84 100644
--- a/opentech/apply/funds/views.py
+++ b/opentech/apply/funds/views.py
@@ -1,4 +1,6 @@
+import mimetypes
 from copy import copy
+from wsgiref.util import FileWrapper
 
 from django.conf import settings
 from django.contrib.auth.decorators import login_required, permission_required
@@ -8,13 +10,13 @@ from django.contrib import messages
 from django.core.exceptions import PermissionDenied
 from django.core.files.storage import get_storage_class
 from django.db.models import Count, F, Q
-from django.http import HttpResponseRedirect, Http404
+from django.http import HttpResponseRedirect, Http404, StreamingHttpResponse
 from django.shortcuts import get_object_or_404
 from django.urls import reverse_lazy
 from django.utils.decorators import method_decorator
 from django.utils.text import mark_safe
 from django.utils.translation import ugettext_lazy as _
-from django.views.generic import DetailView, FormView, ListView, UpdateView, DeleteView, RedirectView
+from django.views.generic import DetailView, FormView, ListView, UpdateView, DeleteView, View
 
 from django_filters.views import FilterView
 from django_tables2.views import SingleTableMixin
@@ -831,15 +833,32 @@ class SubmissionDeleteView(DeleteView):
         return response
 
 
-class SubmissionPrivateMediaRedirectView(UserPassesTestMixin, RedirectView):
+class SubmissionPrivateMediaView(UserPassesTestMixin, View):
 
-    def get_redirect_url(self, *args, **kwargs):
+    def get(self, *args, **kwargs):
         submission_id = kwargs['submission_id']
         field_id = kwargs['field_id']
         file_name = kwargs['file_name']
         file_name_with_path = f'submission/{submission_id}/{field_id}/{file_name}'
 
-        return submission_storage.url(file_name_with_path)
+        submission_file = submission_storage.open(file_name_with_path)
+        wrapper = FileWrapper(submission_file)
+        encoding_map = {
+            'bzip2': 'application/x-bzip',
+            'gzip': 'application/gzip',
+            'xz': 'application/x-xz',
+        }
+        content_type, encoding = mimetypes.guess_type(file_name)
+        # Encoding isn't set to prevent browsers from automatically uncompressing files.
+        content_type = encoding_map.get(encoding, content_type)
+        content_type = content_type or 'application/octet-stream'
+        # From Django 2.1, we can use FileResponse instead of StreamingHttpResponse
+        response = StreamingHttpResponse(wrapper, content_type=content_type)
+
+        response['Content-Disposition'] = f'filename={file_name}'
+        response['Content-Length'] = submission_file.size
+
+        return response
 
     def test_func(self):
         submission_id = self.kwargs['submission_id']