Files And Images¶
Description
How to program files and image fields for z3c.forms
and Dexterity content types.
Introduction¶
This chapter discuss about file uploads and downloads using zope.schema
based
forms and content with Dexterity content subsystem.
Note
These instructions apply for Plone 5 and forward. These instructions do not apply for Archetypes content or PloneFormGen.
For more introduction information, see:
Simple Content Item File Or Image Field¶
Example¶
Simple CSV file Upload Form¶
plone.namedfile is used for the upload field.
Then the the upload is accepted and the file processed.
A view with the form is registered using the configure.zcml
file.
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:browser="http://namespaces.zope.org/browser"
xmlns:plone="http://namespaces.plone.org/plone"
i18n_domain="example.dexterityforms">
...
<browser:page
for="Products.CMFCore.interfaces.ISiteRoot"
name="import_csv"
permission="cmf.ManagePortal"
class=".importcsv.ImportCSVForm"
/>
</configure>
Create a module named importcsv.py, and add the following code to it
# -*- coding: utf-8 -*-
from plone.autoform import form
from plone.namedfile.field import NamedFile
from Products.CMFPlone import PloneMessageFactory as _
from Products.statusmessages.interfaces import IStatusMessage
import csv
import StringIO
import z3c.form.button
class IImportCSVFormSchema(form.Schema):
""" Define fields used on the form """
csv_file = NamedFile(title=_(u'CSV file'))
class ImportCSVForm(form.SchemaForm):
""" A sample form showing how to mass import users using an uploaded CSV file.
"""
name = _(u'Import Companies') # Form label
schema = IImportCSVFormSchema # Form schema
ignoreContext = True
def processCSV(self, data):
"""
"""
reader = csv.reader(
StringIO.StringIO(data),
delimiter=',',
dialect='excel',
quotechar='"'
)
header = reader.next()
updated = 0
for row in reader:
# process the data here as needed for the specific case
for idx, name in header:
value = row[idx]
updated += 1
return updated
@z3c.form.button.buttonAndHandler(_('Import CSV'), name='import')
def importCSV(self, action):
""" Create and handle form button
"""
# Extract form field values and errors from HTTP request
data, errors = self.extractData()
if errors:
self.status = self.formErrorsMessage
return
# get the actual data
file = data["csv_file"].data
# do the processing
number = self.processCSV(file)
# If everything was ok post success note
# Note you can also use self.status here unless you do redirects
if number is not None:
# mark only as finished if we get the new object
IStatusMessage(
self.request
).addStatusMessage(
_(u'Processed: {0}').format(number),
'info'
)
Programmatically Filling A Field With Content¶
Given a field plone.namedfile.field.NamedBlobFile
named some_file
.
It can be filled programmatically with data by creating a blob first.
# Attention, this is the blob-file object itself,
# opposed to the field with the same class-name used above!
from plone.namedfile.file import NamedBlobFile
blob_data = NamedBlobFile(some_data_string, filename=u'video.mp4')
It can be set on some persistent context, like an arbitary dexterity content type.
context.some_file = blob_data
Getting Download URLs¶
Simple Download URLs¶
To create a download link for file and image fields of Dexterity content types
the @@download
view can be used.
The common schema is http://host/path/to/filecontent/@@download/FIELDNAME
.
To get a URL containing the original filename it may be appended this way:
http://host/path/to/filecontent/@@download/FIELDNAME/FILENAME.EXT
.
As in the example below, the original uploaded filename may be used. But a new/custom filename is fine too.
<!--
Precondition: Custom content type with a "video_file" field.
Flowplayer JavaScript installed.
Renders: Link to video file, only if it's uploaded to this context item.
-->
<tal:if define="video_file nocall:context/video_file"
tal:condition="nocall:video_file">
<a class="flow-player"
tal:attributes="href string:${context/absolute_url}/@@download/video_file/${video_file/filename}">
Video
</a>
</tal:if>
Timestamped Download URL¶
You need to expose file content to the site user through a view and then refer to the URL of the view in your HTML template.
There are some tricks you need to keep in mind:
All file download URLs should be timestamped, or the re-upload file change will not be reflected in the browser.
You might want to serve different file types from different URLs and set special HTTP headers for them.
Example:
from plone.namedfile.interfaces import INamedBlobFile
# <browser:page> providing blob object traverse and streaming
# using download_blob() function below
download_view_name = "@@header_animation_helper"
def construct_url(context, animation_object_id, blob):
""" Construct download URL for delivering files.
Adds file upload timestamp to URL to prevent cache issues.
@param context: Content object who own the files
@param video_object_id: Unique identified for the animation in the
animation container
(in the case there are several of them)
@param field_value: NamedBlobFile or NamedBlobImage or None
@return: None if there is no blob or the blob field value is empty
(file has been removed from admin interface)
"""
if blob is None:
return None
# This case occurs when the file has been removed thorugh form interfaces
# (one of keep, replace, remove options on file widget)
if animation_object_id is None:
raise RuntimeError('Cannot have None id')
# Timestamping prevents caching issues,
# otherwise the browser shows the old version after reupload
if hasattr(blob, "_p_mtime"):
# Zope persistency timestamp is float seconds since epoch
timestamp = blob._p_mtime
else:
timestamp = ''
# We have different BrowserView methods for download depending on the
# file type
if INamedBlobFile.providedBy(blob):
func_name = "download_video"
else:
func_name = "download_image"
# This looks like
return '{0}/{1}/{2}?timestamp={3}'.format(
context.absolute_url(),
download_view_name,
func_name,
timestamp
)
Streaming File Data¶
File data is delivered to the browser as a stream. The view function returns a streaming iterator instead of raw data.
This greatly reduces the latency and memory usage when the file should not be buffered as a whole to memory before sending.
Example of a streaming browser view:
from plone.namedfile.utils import set_headers
from plone.namedfile.utils import stream_data
from Products.Five import BrowserView
from zope.publisher.interfaces import NotFound
class StreamingFieldDownload(BrowserView):
""" Stream file and image downloads.
"""
def __init__(self, context, request):
self.context = context
self.request = request
def __call__(self):
"""Stream BLOB of context ``file`` field to the browser.
@param file: Blob object
"""
blob = self.context.file
if blob is None:
raise NotFound('No file present')
# Try determine blob name and default to "context_id_download."
# This is only visible if the user tried to save the file to local
# computer.
filename = getattr(blob, 'filename', self.context.id + '_download')
# Sets Content-Type and Content-Length
set_headers(blob, self.request.response)
# Set Content-Disposition
self.request.response.setHeader(
'Content-Disposition',
'inline; filename={0}'.format(filename)
)
return stream_data(blob)
POSKeyError On Missing Blob¶
A POSKeyError
is raised when you try to access blob attributes,
but the actual file is not available on the disk.
You can still load the blob object itself fine (as it’s being stored in the ZODB, not on the filesystem).
Example traceback snippet:
Module ZPublisher.Publish, line 119, in publish
Module ZPublisher.mapply, line 88, in mapply
Module ZPublisher.Publish, line 42, in call_object
Module plone.app.headeranimation.browser.views, line 92, in download_image
Module plone.app.headeranimation.browser.views, line 75, in _download_blob
Module plone.app.headeranimation.browser.download, line 90, in download_blob
Module plone.namedfile.utils, line 58, in stream_data
Module ZODB.Connection, line 811, in setstate
Module ZODB.Connection, line 876, in _setstate
Module ZODB.blob, line 623, in loadBlob
POSKeyError: 'No blob file'
This might occur for example because you have copied the Data.fs
file to another computer,
but not (all) blob files.
You probably want to catch POSKeyError
s and return something more sane instead
from plone.namedfile.utils import set_headers
from plone.namedfile.utils import stream_data
from Products.Five import BrowserView
from ZODB.POSException import POSKeyError
from zope.publisher.interfaces import NotFound
import logging
logger = logging.getLogger(__name__)
class StreamingFieldDownload(BrowserView):
""" Stream file and image downloads.
"""
def __init__(self, context, request):
self.context = context
self.request = request
def __call__(self):
"""Stream BLOB of context ``file`` field to the browser.
@param file: Blob object
"""
blob = self.context.file
if blob is None:
raise NotFound('No file present')
# Try determine blob name and default to "context_id_download."
# This is only visible if the user tried to save the file to local
# computer.
try:
filename = getattr(blob, 'filename', self.context.id + '_download')
# Sets Content-Type and Content-Length
set_headers(blob, self.request.response)
# Set Content-Disposition
self.request.response.setHeader(
'Content-Disposition',
'inline; filename={0}'.format(filename)
)
return stream_data(blob)
except POSKeyError:
logger.exception(
'Could not load blob for {0}'.format(str(self.context))
)
raise NotFound('Blob file is missing in blob storage.')
See also
Widget Download URLs¶
Some things you might want to keep in mind when playing with forms and images:
Image data might be incomplete (no width/height) during the first
POST
.Image URLs might change in the middle of request (image was updated).
If your form content is something else than traversable context object then you must fix file download URLs manually.
Migrating Custom Content For Blobs¶
Some hints how to migrate your custom content:
Form Encoding¶
Warning
Make sure that all forms containing file content are posted as enctype="multipart/form-data"
.
If you don’t do this, Zope decodes request POST
values as string input and you get either empty strings or filenames as your file content data.
The older plone.app.z3cform
templates do not necessarily declare enctype
,
meaning that you need to use a custom page template file for forms doing uploads.
Example correct form header:
<form action="."
enctype="multipart/form-data"
method="post"
tal:attributes="action request/getURL">