Export Plone 3 folder to zip file


To add functionality to a Plone 3 site to enable a user to download the contents of a folder and all subfolders as a .zip file.

Environment in which this tool was developed

Plone 3.3.1
Zope 2.10.9-final
Python 2.4.6

Important Notes

Excluded Content

The following types of content are excluded from the .zip file:

  • Links
  • Events
  • Empty Folders

Permissions and workflow state

The .zip file created by this tool includes only files and folders.  Permissions and workflow states on folders or content items are not retained in any way.  Permissions, however, are not bypassed — i.e., only content objects on which the current user has the View permission are included.

HTML Documents: Pages and News Items

“Pages” (a.k.a. “Documents”) and “News Items” are processed in the following way:

  • If the .html file extension is missing, it is added
  • A complete, yet simple, HTML document is created for the content — i.e., the Plone “wrapper” is removed.
  • The body of the document consists of the “cooked” document content, with the addition of an H1 element at the top containing the document title.
    If the document has a description, it is inserted in a paragraph element below the title.
  • The document creator and last modified date are added below the document content.

The Pieces

External module

The easiest way to implement is to create an “old-style” Zope product, which is simply a Python package, and put it in the products directory.  In my case, that directory is /opt/plone/zeoserver/products.  You may want to put an empty file called refresh.txt in the root of the package — this can help in development to avoid having to restart Zope clients to pick up changes — although you do still have to re-save the External Method that references the module, and I found that restarting the clients was often required anyway.  In the Python package, create a subdirectory, not a subpackage, called Extensions, and create your code module there.

For purposes of this article, I’ll call the product package is ExportTool, and the module export.py.

Note: To use the code as-is, you’ll need to create /opt/plone/temp and make it writeable by the user Unix under which the Plone clients run.  Alternatively, you could assign the TEMPDIR global variable in the module to tempfile.gettempdir().


import cgi
import os
import shutil
import subprocess
import tempfile

TEMPDIR = '/opt/plone/temp'

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
    <meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
      Created by: %(creator)s<br/>
      Last modified: %(modified)s

def export_folder(context, response):

    transform_tool = context.portal_transforms
    # temp dir for this export job
    tempdir = tempfile.mkdtemp(dir=TEMPDIR)

    def _export(folder, tempdir):
        # create dir into which folder contents will be exported as files
        folder_path = folder.getPhysicalPath()[1:]
        export_dir = os.path.join(tempdir, os.path.join(*folder_path))
        for obj in folder.getFolderContents(full_objects=True):
            if obj.portal_type == 'Folder':
                # recursive call
                _export(obj, tempdir)
            elif obj.portal_type in ['Image', 'File', 'Document', 'News Item']:
                filename = obj.getId()
                if obj.portal_type in ['Image', 'File']:
                    content = obj.data
                    if not filename.endswith('.html'):
                        filename += '.html'
                    body = obj.CookedBody()
                    description = obj.Description()
                    if description:
                        body = '<p>%s</p>\n%s' % (cgi.escape(description, quote=True), body)
                    content = DOC_TEMPLATE % {
                        'body': body,
                        'title': cgi.escape(obj.Title(), quote=True),                        
                        'modified': context.toLocalizedTime(obj.ModificationDate(), long_format=1),
                        'creator': obj.Creator(),
                outfile = open(os.path.join(export_dir, filename), 'wb')

        # export the content
        _export(context, tempdir)
        # create a zip file            
        zipprefix = '-'.join(context.getPhysicalPath()[1:])
        subprocess.call(['/usr/bin/zip', '-r', zipprefix, 'intranet'])
        zipname = zipprefix + '.zip'
        zipped = os.path.join(tempdir, zipname)
        # set response headers
        response.setHeader('Content-Type', 'application/zip')
        response.setHeader('Content-Disposition', 'attachment; filename=%s' % zipname)
        # write zip file to response
        z = open(zipped, 'rb')
        while 1:
            chunk = z.read(8192)
            if chunk:
        # delete the temp dir

Zope External Method

id: export_folder_to_zip
Module Name: ExportTool.export
Function Name: export_folder

Zope Script (Python)

id: exportFolderToZip


if context.portal_type == 'Folder' and len(context.getPhysicalPath()) > 3:
    container.export_folder_to_zip(context, container.REQUEST.RESPONSE)

Note: The guard limiting path length is to prevent downloading top-level folders (path element 1 is a slash, 2 is the Plone site, 3 is the first folder).

Add Action to Folder portal type

Title: Zip
Id: zip
URL (Expression): string:${folder_url}/exportFodlerToZip
Condition (Expression): (blank)
Permission: Modify portal content
Category: Object
Visible? (check)