Edgewall Software

Ticket #7078: 7078-download-all-r10500.patch

File 7078-download-all-r10500.patch, 8.9 KB (added by rblank, 16 months ago)

Updated patch.

  • trac/attachment.py

    diff --git a/trac/attachment.py b/trac/attachment.py
    a b  
    1818 
    1919from __future__ import with_statement 
    2020 
     21from cStringIO import StringIO 
    2122from datetime import datetime 
    2223import os.path 
    2324import re 
    from trac.mimeview import * 
    3637from trac.perm import PermissionError, IPermissionPolicy 
    3738from trac.resource import * 
    3839from trac.search import search_to_sql, shorten_result 
    39 from trac.util import get_reporter_id, create_unique_file 
     40from trac.util import content_disposition, create_unique_file, get_reporter_id 
    4041from trac.util.datefmt import format_datetime, from_utimestamp, \ 
    4142                              to_datetime, to_utimestamp, utc 
    4243from trac.util.text import exception_to_unicode, pretty_size, print_table, \ 
    4344                           unicode_quote, unicode_unquote 
    4445from trac.util.translation import _, tag_ 
    45 from trac.web import HTTPBadRequest, IRequestHandler 
     46from trac.web import HTTPBadRequest, IRequestHandler, RequestDone 
    4647from trac.web.chrome import (INavigationContributor, add_ctxtnav, add_link, 
    4748                             add_stylesheet, web_context) 
    4849from trac.web.href import Href 
    class AttachmentModule(Component): 
    398399    # IRequestHandler methods 
    399400 
    400401    def match_request(self, req): 
    401         match = re.match(r'/(raw-)?attachment/([^/]+)(?:/(.*))?$', 
     402        match = re.match(r'/(raw-|zip-)?attachment/([^/]+)(?:/(.*))?$', 
    402403                         req.path_info) 
    403404        if match: 
    404             raw, realm, path = match.groups() 
    405             if raw: 
    406                 req.args['format'] = 'raw' 
     405            format, realm, path = match.groups() 
     406            if format: 
     407                req.args['format'] = format[:-1] 
    407408            req.args['realm'] = realm 
    408409            if path: 
    409410                req.args['path'] = path 
    class AttachmentModule(Component): 
    436437        add_ctxtnav(req, _('Back to %(parent)s', parent=parent_name),  
    437438                    parent_url) 
    438439         
    439         if action != 'new' and not filename:  
    440             # there's a trailing '/', show the list 
    441             return self._render_list(req, parent) 
     440        if not filename: # there's a trailing '/' 
     441            if req.args.get('format') == 'zip': 
     442                self._download_as_zip(req, parent) 
     443            elif action != 'new': 
     444                return self._render_list(req, parent) 
    442445 
    443446        attachment = Attachment(self.env, parent.child('attachment', filename)) 
    444447         
    class AttachmentModule(Component): 
    481484                attachments.append(attachment) 
    482485        new_att = parent.child('attachment') 
    483486        return {'attach_href': get_resource_url(self.env, new_att, 
    484                                                 context.href, action='new'), 
     487                                                context.href), 
     488                'download_href': get_resource_url(self.env, new_att, 
     489                                                  context.href, format='zip'), 
    485490                'can_create': 'ATTACHMENT_CREATE' in context.perm(new_att), 
    486491                'attachments': attachments, 
    487492                'parent': context.resource} 
    class AttachmentModule(Component): 
    565570            return None 
    566571        format = kwargs.get('format') 
    567572        prefix = 'attachment' 
    568         if format == 'raw': 
     573        if format in ('raw', 'zip'): 
    569574            kwargs.pop('format') 
    570             prefix = 'raw-attachment' 
     575            prefix = format + '-attachment' 
    571576        parent_href = unicode_unquote(get_resource_url(self.env, 
    572577                            resource.parent(version=None), Href(''))) 
    573         if not resource.id:  
     578        if not resource.id: 
    574579            # link to list of attachments, which must end with a trailing '/'  
    575580            # (see process_request) 
    576             return href(prefix, parent_href) + '/' 
     581            return href(prefix, parent_href, '', **kwargs) 
    577582        else: 
    578583            return href(prefix, parent_href, resource.id, **kwargs) 
    579584 
    class AttachmentModule(Component): 
    705710        return {'mode': 'new', 'author': get_reporter_id(req), 
    706711            'attachment': attachment, 'max_size': self.max_size} 
    707712 
     713    def _download_as_zip(self, req, parent, attachments=None): 
     714        req.send_response(200) 
     715        req.send_header('Content-Type', 'application/zip') 
     716        filename = 'attachments-%s-%s.zip' % \ 
     717                   (parent.realm, re.sub(r'[/\\:]', '-', unicode(parent.id))) 
     718        req.send_header('Content-Disposition', 
     719                        content_disposition('inline', filename)) 
     720        if attachments is None: 
     721            attachments = Attachment.select(self.env, parent.realm, parent.id) 
     722 
     723        from zipfile import ZipFile, ZipInfo, ZIP_DEFLATED 
     724 
     725        buf = StringIO() 
     726        zipfile = ZipFile(buf, 'w', ZIP_DEFLATED) 
     727        for attachment in attachments: 
     728            if 'ATTACHMENT_VIEW' not in req.perm(attachment.resource): 
     729                continue 
     730            zipinfo = ZipInfo() 
     731            zipinfo.filename = attachment.filename.encode('utf-8') 
     732            zipinfo.date_time = attachment.date.utctimetuple()[:6] 
     733            zipinfo.compress_type = ZIP_DEFLATED 
     734            zipinfo.comment = attachment.description 
     735            zipinfo.external_attr = 0644 << 16L # needed since Python 2.5 
     736            with attachment.open() as fd: 
     737                zipfile.writestr(zipinfo, fd.read()) 
     738        zipfile.close() 
     739 
     740        zip_str = buf.getvalue() 
     741        req.send_header("Content-Length", len(zip_str)) 
     742        req.end_headers() 
     743        req.write(zip_str) 
     744        raise RequestDone() 
     745 
    708746    def _render_list(self, req, parent): 
    709747        data = { 
    710748            'mode': 'list', 
    class AttachmentModule(Component): 
    734772 
    735773            # Eventually send the file directly 
    736774            format = req.args.get('format') 
    737             if format in ('raw', 'txt'): 
     775            if format == 'zip': 
     776                self._download_as_zip(req, attachment.resource.parent, 
     777                                      [attachment]) 
     778            elif format in ('raw', 'txt'): 
    738779                if not self.render_unsafe_content: 
    739780                    # Force browser to download files instead of rendering 
    740781                    # them, since they might contain malicious code enabling  
  • trac/htdocs/css/trac.css

    diff --git a/trac/htdocs/css/trac.css b/trac/htdocs/css/trac.css
    a b div.trac-grip { 
    445445.attachment #preview { margin-top: 1em } 
    446446 
    447447/* Styles for the list of attachments. */ 
    448 #attachments > div { border: 1px outset #996; padding: 1em } 
    449 #attachments .attachments { margin-left: 2em; padding: 0 } 
     448#attachments > div.attachments { border: 1px outset #996; padding: 1em } 
     449#attachments dl.attachments { margin-left: 2em; padding: 0 } 
    450450#attachments dt { display: list-item; list-style: square; } 
    451451#attachments dd { font-style: italic; margin-left: 0; padding-left: 0; } 
     452#attachments p { margin-left: 2em; } 
    452453 
    453454/* Styles for tabular listings such as those used for displaying directory 
    454455   contents and report results. */ 
  • trac/templates/list_of_attachments.html

    diff --git a/trac/templates/list_of_attachments.html b/trac/templates/list_of_attachments.html
    a b Arguments: 
    2626    <div id="attachments" py:choose=""> 
    2727      <py:when test="compact and alist.attachments"> 
    2828        <h3 class="${foldable and 'foldable' or None}">Attachments</h3> 
    29         <ul> 
    30           <py:for each="attachment in alist.attachments"> 
    31             <li> 
    32               ${show_one_attachment(attachment)} 
    33               <q py:if="compact and attachment.description">${wiki_to_oneliner(context, attachment.description)}</q> 
    34             </li> 
    35           </py:for> 
    36         </ul> 
     29        <div> 
     30          <ul> 
     31            <py:for each="attachment in alist.attachments"> 
     32              <li> 
     33                ${show_one_attachment(attachment)} 
     34                <q py:if="compact and attachment.description">${wiki_to_oneliner(context, attachment.description)}</q> 
     35              </li> 
     36            </py:for> 
     37          </ul> 
     38          <p py:if="alist.attachments"> 
     39            Download all attachments as: <a rel="nofollow" href="$alist.download_href">.zip</a> 
     40          </p> 
     41        </div> 
    3742      </py:when> 
    3843      <py:when test="not compact"> 
    3944        <h2 class="${foldable and 'foldable' or None}">Attachments</h2> 
    40         <div py:if="alist.attachments or alist.can_create"> 
     45        <div py:if="alist.attachments or alist.can_create" class="attachments"> 
    4146          <dl py:if="alist.attachments" class="attachments"> 
    4247            <py:for each="attachment in alist.attachments"> 
    4348              <dt>${show_one_attachment(attachment)}</dt> 
    Arguments: 
    4651              </dd> 
    4752            </py:for> 
    4853          </dl> 
     54          <p py:if="alist.attachments"> 
     55            Download all attachments as: <a rel="nofollow" href="$alist.download_href">.zip</a> 
     56          </p> 
    4957          <xi:include href="attach_file_form.html"/> 
    5058        </div> 
    5159      </py:when>