===========
dolmen.file
===========
``dolmen.file`` allows you to manage and store files within the ZODB.
It takes the core functionalities of ``zope.app.file``, and simplifies
them, using Grok for views and adapters registrations.
Compatibility
=============
In order to make sure that our `File` implementation is complete and
functional, we test it against the original ``zope.app.file`` tests::
>>> from dolmen.file import NamedFile, INamedFile, FileChunk
Let's test the constructor::
>>> file = NamedFile()
>>> file.contentType
''
>>> file.data
''
>>> file = NamedFile('Foobar')
>>> file.contentType
''
>>> file.data
'Foobar'
>>> file = NamedFile('Foobar', 'text/plain')
>>> file.contentType
'text/plain'
>>> file.data
'Foobar'
>>> file = NamedFile(data='Foobar', contentType='text/plain')
>>> file.contentType
'text/plain'
>>> file.data
'Foobar'
Let's test the mutators::
>>> file = NamedFile()
>>> file.contentType = 'text/plain'
>>> file.contentType
'text/plain'
>>> file.data = 'Foobar'
>>> file.data
'Foobar'
>>> file.data = None
Traceback (most recent call last):
...
TypeError: Cannot set None data on a file.
Let's test large data input::
>>> file = NamedFile()
Insert as string:
>>> file.data = 'Foobar'*60000
>>> file.size
360000
>>> file.data == 'Foobar'*60000
True
Insert data as FileChunk::
>>> fc = FileChunk('Foobar'*4000)
>>> file.data = fc
>>> file.size
24000
>>> file.data == 'Foobar'*4000
True
Insert data from file object::
>>> import cStringIO
>>> sio = cStringIO.StringIO()
>>> sio.write('Foobar'*100000)
>>> sio.seek(0)
>>> file.data = sio
>>> file.size
600000
>>> file.data == 'Foobar'*100000
True
Last, but not least, verify the interface implementation::
>>> from zope.interface.verify import verifyClass
>>> INamedFile.implementedBy(NamedFile)
True
>>> verifyClass(INamedFile, NamedFile)
True
Naming
======
When no name is provided, the fallback is a simple empty unicode
string::
>>> file = NamedFile('Foobar')
>>> file.contentType
''
>>> file.data
'Foobar'
>>> file.filename
u''
To specifiy a filename, we can give it to the constructor::
>>> file = NamedFile('Foobar', filename='foobar.txt')
>>> file.data
'Foobar'
>>> file.filename
u'foobar.txt'
The filename can be both unicode or simple string::
>>> file = NamedFile('Foobar', filename=u'foobar.txt')
>>> file.data
'Foobar'
>>> file.filename
u'foobar.txt'
The filename provided had an extension : 'txt'. This extension is used
by the NamedFile, while instanciated, to try and guess the mimetype of
the data::
>>> file.contentType
'text/plain'
The filename can be set later, but this won't trigger the mime
type guess::
>>> file.filename = u"something.zip"
>>> file.filename
u'something.zip'
>>> file.contentType
'text/plain'
Size
====
To represent the size of the stored data, ``dolmen.file`` uses a
normalized adaptation, based on ``zope.size`` definitions::
>>> from zope.size import ISized
>>> sized = ISized(file)
>>> sized
<dolmen.file.size.Sized object at ...>
>>> sized.sizeForSorting()
('byte', 6)
>>> sized.sizeForDisplay()
u'1 KB'
Access
======
In order to access our file, ``dolmen.file`` provides a view called
`file_publish` that sets the proper headers and returns the
data. Let's set up a simple environment to test that behavior::
>>> from zope.component.hooks import getSite
>>> from zope.component import getMultiAdapter
>>> from zope.publisher.browser import TestRequest
>>> root = getSite()
>>> root['myfile'] = NamedFile('Foobar', filename='foobar.txt')
>>> myfile = root['myfile']
The `file_publish` view will adapt a INamedFile and a request and,
when called, will return the data.
>>> request = TestRequest()
>>> view = getMultiAdapter((myfile, request), name='file_publish')
>>> view
<dolmen.file.access.FilePublisher object at ...>
In the update of the view, the headers are set properly, using the
info of the file::
>>> view.update()
>>> for key, value in view.response.getHeaders(): print key, repr(value)
X-Powered-By 'Zope (www.zope.org), Python (www.python.org)'
Content-Length '6'
Content-Type 'text/plain'
Content-Disposition 'attachment; filename="foobar.txt"'
>>> view.render()
'Foobar'
Field, download and security
============================
In a site, the file object is rarely accessed directly. Often, it's
just a part of a more complex object. For that matter, we have three
dedicated components: the field, the property and the traverser.
Field and Property
------------------
A property is provided to allow a transparent use of a INamedFile component.
Working exemple
~~~~~~~~~~~~~~~
>>> from persistent import Persistent
>>> from dolmen.file import FileProperty, FileField
>>> from zope.interface import Interface, implements
>>> class IContent(Interface):
... binary = FileField(title=u"Binary data")
>>> class MyContent(Persistent):
... implements(IContent)
... binary = FileProperty(IContent['binary'])
>>> root['mammoth'] = MyContent()
>>> manfred = root['mammoth']
>>> manfred.binary = FileChunk('Foobar')
>>> manfred.binary
<dolmen.file.file.NamedFile object at ...>
>>> manfred.binary.data
'Foobar'
Custom factory
~~~~~~~~~~~~~~
>>> class MyFile(NamedFile):
... """My own file type.
... """
>>> class CustomContent(object):
... implements(IContent)
... binary = FileProperty(IContent['binary'], factory=MyFile)
>>> custom = CustomContent()
>>> custom.binary = FileChunk('Foobar')
>>> custom.binary
<MyFile object at ...>
Error
~~~~~
>>> class MyFalseFile(object):
... """My own file type.
... """
>>> class FaultyContent(object):
... implements(IContent)
... binary = FileProperty(IContent['binary'], factory=MyFalseFile)
Traceback (most recent call last):
...
ValueError: Provided factory is not a valid INamedFile
Fields
~~~~~~
There are two fields provided by `dolmen.file`: the FileField and the
ImageField. They are just logical separation but have a common base::
>>> from dolmen.file import IImageField, IFileField, ImageField
>>> IImageField.extends(IFileField)
True
>>> isinstance(ImageField(), FileField)
True
Traversal
---------
The traverser will take care of both the fetching and the security
checking, while accessing your data. The basic permission used to
check the availability of the data, is `zope.View`.
Here, we set up two principals to test this. 'jason' is a logged in
member with no rights while 'judith' has the `zope.View` permission
granted::
>>> import zope.security.management as security
>>> from zope.traversing.interfaces import ITraversable
>>> from zope.security.testing import Principal, Participation
>>> judith = Principal('zope.judith', 'Judith')
>>> jason = Principal('zope.jason', 'Jason')
We create the interaction and try to traverse to our binary data::
>>> security.newInteraction(Participation(jason))
>>> traverser = getMultiAdapter(
... (manfred, request), ITraversable, 'download')
>>> traverser
<dolmen.file.access.DownloadTraverser object at ...>
>>> traverser.traverse('binary')
Traceback (most recent call last):
...
Unauthorized: binary
>>> security.endInteraction()
It fails. An Unauthorized Error is raised. We now try with Judith::
>>> security.newInteraction(Participation(judith))
>>> traverser.traverse('binary')
<dolmen.file.access.FilePublisher object at ...>
Our data is returned, wrapped in a `FilePublisher` view, ready to be
rendered (see the Access section, for more information).
What if we traverse to an unknown field ? Let's try::
>>> traverser.traverse('zorglub')
Traceback (most recent call last):
...
NotFound: Object: <MyContent object at ...>, name: 'zorglub'
Everything is fine : a NotFound error has been raised. If we try to
access a file that is not an INamedFile, we get another error::
>>> traverser.traverse('__name__')
Traceback (most recent call last):
...
LocationError: '__name__ is not a valid INamedFile'
We gracefully end our tests::
>>> security.endInteraction()
Enjoy !
Changelog
=========
0.6 (2010-11-17)
------------------
* Tested for Grok 1.2.
* ``zope.testing`` dependency has been removed.
* The INamedFile factory is now pluggable, in the file property. Tests
have been added to fix that behavior.
0.5.1 (2010-02-28)
------------------
* Added an `ISized` adapter for `INamedFile` objects. Added tests
accordingly.
0.5.0 (2010-02-28)
------------------
* Updated code base to be fully pep8 compatible.
* `zope.app` dependencies have been entirely dropped.
* ``dolmen.file`` is no longer a layer above ``zope.app.file``. It
nows integrates the few features it needed from ``zope.app.file``.
0.4.0 (2009-11-18)
------------------
* Release compatible with ZTK 1.0dev versions. Pinned down the version
of zope.traversing in the setup.py. It now runs on Python 2.6 !
0.3.2 (2009-10-23)
------------------
* Corrected a bug on the clean_filename util function, that would fail
on unicode values. Added a test to fix that behavior.
0.3.1 (2009-10-23)
------------------
* Removed the __parent__ attribution in the property. If you relied on
this, you now have to take care of the location proxying yourself.
0.3.0 (2009-10-23)
------------------
* Changed the filename cleaning method, now exposed in the public
API. We now use a compiled regexp to get the name.
0.2.0 (2009-10-21)
------------------
* Added an image field with the corresponding interface. This was
previously part of ``dolmen.imaging``. The ImageField component is a
simple subclass of the default FileField.
0.1.0 (2009-10-16)
------------------
* Initial release