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 0f9d8bab2fdf721d377eb9939fce29be5d0b0490..07fe086f5fd2fb25cd0daed66f8005554d7510ef 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
@@ -844,15 +846,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']
diff --git a/opentech/storage_backends.py b/opentech/storage_backends.py
index 3a347b4cbf1c2194e2e77dc7fbac240935eb6e62..e7f08555901397cda8232239a2bed3d01b289432 100644
--- a/opentech/storage_backends.py
+++ b/opentech/storage_backends.py
@@ -36,7 +36,7 @@ class PrivateMediaStorage(S3Boto3Storage):
             try:
                 name_parts = name.split('/')
                 return reverse(
-                    'apply:submissions:private_media_redirect', kwargs={
+                    'apply:submissions:serve_private_media', kwargs={
                         'submission_id': name_parts[1], 'field_id': name_parts[2],
                         'file_name': name_parts[3]
                     }