diff --git a/reviewboard/attachments/streaming_tarfile.py b/reviewboard/attachments/streaming_tarfile.py
new file mode 100644
index 0000000000000000000000000000000000000000..45e1d3c1d68adc3dd8a15bfe12e64bd23b93a15d
--- /dev/null
+++ b/reviewboard/attachments/streaming_tarfile.py
@@ -0,0 +1,119 @@
+import tarfile
+import time
+
+from django.utils.six.moves import cStringIO as StringIO
+
+
+class FileStream(object):
+    """File stream for streaming reponses (review request attachments)
+
+    This buffer intended for use as an argument to StreamingHTTPResponse
+    and also as a file for TarFile to write into.
+
+    Files are read in by chunks and written to this buffer through TarFile.
+    When there is content to be read from the buffer, it is taken up by
+    StreamingHTTPResponse and the buffer is cleared to prevent storing large
+    chunks of data in memory.
+    """
+    def __init__(self):
+        self.buffer = StringIO()
+        self.offset = 0
+
+    def write(self, s):
+        """Write ``s`` to the buffer and adjust the offset."""
+        self.buffer.write(s)
+        self.offset += len(s)
+
+    def tell(self):
+        """Return the current position of the buffer."""
+        return self.offset
+
+    def close(self):
+        """Close the buffer."""
+        self.buffer.close()
+
+    def pop(self):
+        """Return the current contents of the buffer then clear it."""
+        s = self.buffer.getvalue()
+        self.buffer.close()
+
+        self.buffer = StringIO()
+
+        return s
+
+
+class StreamingTarFile(object):
+    """ A streaming TarFile object for StreamingHTTPReponse.
+
+    For arguments: takes an ```out_filename``` (the filename to write out to)
+    and a list of ```files``` from a review request.
+
+    Handles building TarInfo objects for each file and also writing chunks of
+    files out to a file object (FileStream).
+    """
+
+    def __init__(self, out_filename, files):
+        """Set constants, output filename and list of files to tarball."""
+        self.MODE = 0644
+        self.BLOCK_SIZE = 4096
+        self.out_filename = out_filename
+        self.files = files
+
+    def build_tar_info(self, f):
+        """Build TarInfo object to represent one file in tarball."""
+        tar_info = tarfile.TarInfo(f.filename)
+        tar_info.mode = self.MODE
+        tar_info.size = f.file.size
+
+        try:
+            modified_time = f.file.storage.modified_time(f.file.name)
+            tar_info.mtime = time.mktime(modified_time.timetuple())
+
+        except:
+            pass
+
+        return tar_info
+
+    def stream_build_tar(self, streaming_fp):
+        """Build tarball by writing it's contents out to streaming_fp."""
+        tar = tarfile.TarFile.open(self.out_filename, 'w|gz', streaming_fp)
+
+        for f in self.files:
+            tar_info = self.build_tar_info(f)
+            tar.addfile(tar_info)
+
+            yield
+
+            while True:
+                s = f.file.read(self.BLOCK_SIZE)
+
+                if len(s) > 0:
+                    tar.fileobj.write(s)
+                    yield
+
+                if len(s) < self.BLOCK_SIZE:
+                    blocks, remainder = divmod(tar_info.size,
+                                               tarfile.BLOCKSIZE)
+
+                    if remainder > 0:
+                        tar.fileobj.write(tarfile.NUL *
+                                          (tarfile.BLOCKSIZE - remainder))
+
+                        yield
+
+                        blocks += 1
+
+                    tar.offset += blocks * tarfile.BLOCKSIZE
+                    break
+
+        tar.close()
+        yield
+
+    def generate(self):
+        """Generate FileStream object to stream via StreamingHttpResponse."""
+        streaming_fp = FileStream()
+        for i in self.stream_build_tar(streaming_fp):
+            s = streaming_fp.pop()
+
+            if len(s) > 0:
+                yield(s)
diff --git a/reviewboard/reviews/urls.py b/reviewboard/reviews/urls.py
index 30de1ca9123783f23a9be1164dfbbcb62cb16cd6..52a413c92334f2fc208317b5280b7dc221b47edf 100644
--- a/reviewboard/reviews/urls.py
+++ b/reviewboard/reviews/urls.py
@@ -84,6 +84,11 @@ review_request_urls = patterns(
         'review_file_attachment',
         name='file-attachment'),
 
+    # Download tarball of file attachments
+    url(r'^file/tar/$',
+        'download_review_attachments',
+        name="download-review-attachments"),
+
     # Screenshots
     url(r'^s/(?P<screenshot_id>[0-9]+)/$',
         'view_screenshot',
diff --git a/reviewboard/reviews/views.py b/reviewboard/reviews/views.py
index d3277b2a28107beb38e7e2e83875761d863bb3d1..0aa4aa015331d22b8e4e86f622c8d85f14defb8c 100644
--- a/reviewboard/reviews/views.py
+++ b/reviewboard/reviews/views.py
@@ -14,7 +14,8 @@ from django.http import (Http404,
                          HttpResponseNotFound,
                          HttpResponseNotModified,
                          HttpResponseRedirect,
-                         HttpResponseServerError)
+                         HttpResponseServerError,
+                         StreamingHttpResponse)
 from django.shortcuts import (get_object_or_404, get_list_or_404, render,
                               render_to_response)
 from django.template.context import RequestContext
@@ -39,6 +40,7 @@ from reviewboard.accounts.decorators import (check_login_required,
 from reviewboard.accounts.models import ReviewRequestVisit, Profile
 from reviewboard.attachments.models import (FileAttachment,
                                             FileAttachmentHistory)
+from reviewboard.attachments.streaming_tarfile import StreamingTarFile
 from reviewboard.changedescs.models import ChangeDescription
 from reviewboard.diffviewer.diffutils import (convert_to_unicode,
                                               get_file_chunks_in_range,
@@ -318,6 +320,34 @@ def _get_latest_file_attachments(file_attachments):
 
 @check_login_required
 @check_local_site_access
+def download_review_attachments(request,
+                                review_request_id,
+                                local_site=None,
+                                template_name=None):
+    """Downloads all file attachments on a review request as a tarball.
+
+    All file attachments associated with the given review request ID
+    are tarballed.
+    """
+    review_request, response = _find_review_request(
+        request, review_request_id, local_site)
+
+    if not review_request:
+        return response
+
+    out_filename = 'r%s-attachments.tar.gz' % review_request_id
+
+    stf = StreamingTarFile(out_filename, review_request.file_attachments.all())
+
+    response = StreamingHttpResponse(stf.generate(),
+                                     mimetype='application/x-gzip')
+    response['Content-Disposition'] = ('attachment; filename=%s' % out_filename)
+
+    return response
+
+
+@check_login_required
+@check_local_site_access
 def review_detail(request,
                   review_request_id,
                   local_site=None,
@@ -748,6 +778,7 @@ def review_detail(request,
         'close_description': close_description,
         'close_description_rich_text': close_description_rich_text,
         'issues': issues,
+        'has_attachments': bool(latest_file_attachments),
         'has_diffs': (draft and draft.diffset) or len(diffsets) > 0,
         'file_attachments': latest_file_attachments,
         'all_file_attachments': file_attachments,
diff --git a/reviewboard/static/rb/js/views/reviewRequestEditorView.js b/reviewboard/static/rb/js/views/reviewRequestEditorView.js
index 22d314f94a407f907a5ba1c7500c26b59d07c3ff..400211d0547c1c3a6214b938f794ebc63ed488f8 100644
--- a/reviewboard/static/rb/js/views/reviewRequestEditorView.js
+++ b/reviewboard/static/rb/js/views/reviewRequestEditorView.js
@@ -876,7 +876,9 @@ RB.ReviewRequestEditorView = Backbone.View.extend({
         var $closeDiscarded = this.$('#discard-review-request-link'),
             $closeSubmitted = this.$('#link-review-request-close-submitted'),
             $deletePermanently = this.$('#delete-review-request-link'),
-            $updateDiff = this.$('#upload-diff-link');
+            $updateDiff = this.$('#upload-diff-link'),
+            $downloadAttachments = this.$("#download-attachments-link"),
+            $downloadDiff = this.$('#download-diff-link');
 
         /*
          * We don't want the click event filtering from these down to the
@@ -1406,8 +1408,10 @@ RB.ReviewRequestEditorView = Backbone.View.extend({
      * This simply prevents the click from bubbling up or invoking the
      * default action.
      */
-    _onMenuClicked: function() {
-        return false;
+    _onMenuClicked: function(evt) {
+        if ($(evt.target).attr('href') == '#') {
+            return false;
+        }
     },
 
     _refreshPage: function() {
diff --git a/reviewboard/templates/reviews/review_detail.html b/reviewboard/templates/reviews/review_detail.html
index 6ce0b99aadfa973d498626c4a7ca95c44123a5e3..d246661ea65efc07494952e60e8134ec6aaee409 100644
--- a/reviewboard/templates/reviews/review_detail.html
+++ b/reviewboard/templates/reviews/review_detail.html
@@ -25,9 +25,6 @@
 {%   review_request_action_hooks %}
 {%   review_request_dropdown_action_hooks %}
 {%   include "reviews/review_request_actions_secondary.html" %}
-{%   if has_diffs %}
-   <li class="primary"><a href="diff/raw/">{% trans "Download Diff" %}</a></li>
-{%   endif %}
 {%   include "reviews/review_request_actions_primary.html" %}
 {%   if has_diffs %}
    <li class="primary"><a href="diff/#index_header">{% trans "View Diff" %}</a></li>
diff --git a/reviewboard/templates/reviews/review_request_actions_primary.html b/reviewboard/templates/reviews/review_request_actions_primary.html
index 418b91e3fa80a181e08612d1686fff87fe2d7157..72572514763c246bef478eb628d60cd1713a5080 100644
--- a/reviewboard/templates/reviews/review_request_actions_primary.html
+++ b/reviewboard/templates/reviews/review_request_actions_primary.html
@@ -1,5 +1,20 @@
 {% load i18n %}
 {% if request.user.is_authenticated %}
+
+{%  if has_attachments or has_diffs %}
+ <li class="has-menu">
+  <a class="menu-title" id="downloads-link" href="#">{% trans "Download" %} &#9662;</a>
+  <ul class="menu" style="display: none;">
+{%  if has_attachments %}
+   <li class="primary"><a id="download-attachments-link"
+       href="{% url 'download-review-attachments' review_request.display_id %}">{% trans "Download Attachments" %}</a></li>
+{%  endif %}
+{%  if has_diffs %}
+   <li class="primary"><a id="download-diff-link" href="#">{% trans "Download Diff" %}</a></li>
+{%  endif %}
+  </ul>
+</li>
+{%  endif %}
  <li class="primary"><a id="review-link" href="#">{% trans "Review" %}</a></li>
  <li class="primary"><a id="shipit-link" href="#">{% trans "Ship It!" %}</a></li>
 {% endif %}
