Handling file uploads with zope.app.file and zope.file

This How-to applies to: 1.0
This How-to is intended for: Developer
If you want to handle files in zope, you can use the zope.app.file and zope.file packages.

zope.app.file

Using zope.app.file is a simple way to implement a file uploading feature. If you don't upload large file, it may be enough for you. If you use filestorage, all file data will be stored in the Data.fs. If you are uploading large files, it will consume lots of memory resources.

Edit the configuration file

Add zope.app.file to setup.py
install_requires=['setuptools',
                  'grok',
                  'grokui.admin',
                  'z3c.testsetup',
                  'zope.app.file',
                  # Add extra requirements here
                  ],
And run ./bin/buildout. This will install zope.app.file and make it available to your project.
In a more formal development setting, or if you wish to ensure that you can recreate your exact development environment at a later date, you may wish to pin the zope.app.file package to the specific version you are developing against in your buildout.cfg. You can do this by adding:
[versions]
zope.app.file = 3.4.4

Implement uploading form

Make a Container object to hold uploaded file(s). filecontainer.py is very simple.
import grok

class FileContainer(grok.Container):
    pass
Add a uploading form to filecontainer.py.
import grok
import zope.app.file
from zope.app.container.interfaces import INameChooser

class FileContainer(grok.Container):
    pass

class Upload(grok.AddForm):
    grok.context(FileContainer)
    form_fields = grok.AutoFields(zope.app.file.interfaces.IFile).select('data')

    @grok.action('Add file')
    def add(self, data):
        self.upload(data)
        self.redirect(self.url(self.context.__parent__))

    def upload(self, data):
        fileupload = self.request['form.data']
        if fileupload and fileupload.filename:
            contenttype = fileupload.headers.get('Content-Type')
            file_ = zope.app.file.file.File(data, contenttype)
            # use the INameChooser registered for your file upload container
            filename = INameChooser(container).chooseName(fileupload.filename)
            self.context[filename] = file_
Note that in order for files to have a valid URL in Grok they must not start with the + or @ character, nor can they contain a / character. The "chooseName" method of the INameChooser interface is normally used for choosing filenames, and the default implementation will check for invalid characters, as well as add a number to a filename if it's uploaded multiple times (e.g. myfile.txt, myfile-1.txt, myfile-2.txt). Note that this will only warn you if your filename contains illegal characters, it will also allow spaces in the filename and unicode characters. You might wish to to implement your own INameChooser if you want to enforce a custom filenaming policy for your application.
For example to provide a name chooser which converts leading + and @ characters to 'plus-' and 'at-' text only in the context of uploading files into your FileContainer:
from zope.app.container.interfaces import INameChooser
from zope.app.container.contained import NameChooser

class PrimitiveFilenameChangingNameChooser(grok.Adapter, NameChooser):
    grok.implements(INameChooser)
    grok.context(FileContainer)

    def chooseName(self, name):
        if name.startswith('+'):
            name = 'plus-' + name[1:]
        if name.startswith('@'):
            name = 'at-' + name[1:]
To make file viewable to the browser, zope.app.file provides a default view for the File model objects so that you don't have to do anything.
You can create a working sample upload application by implementing the following code in a python file:
import grok
import zope.app.file
from zope.app.container.interfaces import INameChooser
from zope.app.container.contained import NameChooser

class FileUploadApp(grok.Application, grok.Container):
    def __init__(self):
        super(FileUploadApp, self).__init__()
        self['files'] = FileContainer()

class FileContainer(grok.Container):
    pass

class Index(grok.View):
    grok.context(FileUploadApp)
    def render(self):
        response = "<html><head></head><body><h1>File List</h1><a href='upload'>Upload File</a><ul>"
        for filename in self.context['files'].keys():
            response += "<li><a href='files/" + filename + "'>" + filename + "</li>"
        response += "</ul></body></html>"
        return response

class Upload(grok.AddForm):
    grok.context(FileUploadApp)
    form_fields = grok.AutoFields(zope.app.file.interfaces.IFile).select('data')

    @grok.action('Upload')
    def add(self, data):
        if len(data) > 0:
            self.upload(data)
        self.redirect(self.url(self.context))

    def upload(self, data):
        fileupload = self.request['form.data']
        if fileupload and fileupload.filename:
            contenttype = fileupload.headers.get('Content-Type')
            file_ = zope.app.file.file.File(data, contenttype)
            # use the INameChooser registered for your file upload container
            filename = INameChooser(self.context['files']).chooseName(fileupload.filename, None)
            self.context['files'][filename] = file_

class PrimitiveFilenameChangingNameChooser(grok.Adapter, NameChooser):
    grok.context(FileUploadApp)
    grok.implements(INameChooser)
    grok.adapts(FileContainer)

    def chooseName(self, name):
        if name.startswith('+'):
            name = 'plus-' + name[1:]
        if name.startswith('@'):
            name = 'at-' + name[1:]
(For a production application, you would want to use a page template to create your file list and provide for editing and deleting files, but that is outside the scope of this how to.)

zope.file

Next is zope.file. This is more efficient than zope.app.file, but requires blob storage.

Edit the configuration file

The buildout created by Grokproject is already configured to handle Blob storage. If you want to change the location of the blob storage directory, you can edit the buildout.cfg file under the "zope_conf" section.
[zope_conf]
recipe = collective.recipe.template
input = etc/zope.conf.in
output = ${buildout:parts-directory}/etc/zope.conf
filestorage = ${buildout:directory}/var/filestorage
blobstorage = ${buildout:directory}/var/blobstorage
By default, the blobstorage directory will be created your project directory.
Next, Add zope.file and zope.mimetype to setup.py.
install_requires=['setuptools',
                  'grok',
                  'grokui.admin',
                  'z3c.testsetup',
                  'zope.file',
                  'zope.mimetype',
                  # Add extra requirements here
                  ],
And run ./bin/buildout. This will enable blob support to filestorage and install zope.file and zope.mimetype, make them available to your project.

Implement uploading form for FileContainer class and view form for File class

We will use the same filecontainer.py and FileContainer class, but modify them to use zope.file in a working sample application:
import grok
import zope.schema
import zope.file.file
import zope.file.upload
import zope.file.download
from zope.app.container.interfaces import INameChooser
from zope.app.container.contained import NameChooser

class FileUploadApp(grok.Application, grok.Container):
    def __init__(self):
        super(FileUploadApp, self).__init__()
        self['files'] = FileContainer()

class FileContainer(grok.Container):
    pass

class Upload(grok.AddForm):
    grok.context(FileUploadApp)
    form_fields = grok.Fields(
      zope.schema.Bytes(__name__='data',
                        title=u'Upload data',
                        description=u'Upload file',),
      )

    @grok.action('Upload')
    def add(self, data):
        if len(data) > 0:
            self.upload(data)
        self.redirect(self.url(self.context))

    def upload(self, data):
        fileupload = self.request['form.data']
        if fileupload and fileupload.filename:
            contenttype = fileupload.headers.get('Content-Type')
            file_ = zope.file.file.File()
            zope.file.upload.updateBlob(file_, fileupload)
            # use the INameChooser registered for your file upload container
            filename = INameChooser(self.context['files']).chooseName(fileupload.filename, None)
            self.context['files'][filename] = file_

class Index(grok.View):
    grok.context(FileUploadApp)
    def render(self):
        response = "<html><head></head><body><h1>File List</h1><a href='upload'>Upload File</a><ul>"
        for filename in self.context['files'].keys():
            response += "<li><a href='files/" + filename + "'>" + filename + "</li>"
        response += "</ul></body></html>"
        return response

class PrimitiveFilenameChangingNameChooser(grok.Adapter, NameChooser):
    grok.context(FileUploadApp)
    grok.implements(INameChooser)
    grok.adapts(FileContainer)

    def chooseName(self, name):
        if name.startswith('+'):
            name = 'plus-' + name[1:]
        if name.startswith('@'):
            name = 'at-' + name[1:]

class FileIndex(zope.file.download.Display, grok.View):
    grok.name('index.html')
    grok.context(zope.file.file.File)
    def render(self):
        return self()
FileIndex class is a view class for File class. Because, there is no default view class for File class. Now, you can upload large file to zope and see it. All file data will be stored in the blob directory.

No comments:

Post a Comment