[Checkins] SVN: hurry.file/ Moving this over from codespeak.net for better maintenance by the
Martijn Faassen
faassen at infrae.com
Tue Sep 16 17:47:16 EDT 2008
Log message for revision 91194:
Moving this over from codespeak.net for better maintenance by the
Zope community.
Changed:
A hurry.file/
A hurry.file/trunk/
A hurry.file/trunk/CHANGES.txt
A hurry.file/trunk/CREDITS.txt
A hurry.file/trunk/README.txt
A hurry.file/trunk/ZopePublicLicense.txt
A hurry.file/trunk/bootstrap.py
A hurry.file/trunk/buildout.cfg
A hurry.file/trunk/setup.py
A hurry.file/trunk/src/
A hurry.file/trunk/src/hurry/
A hurry.file/trunk/src/hurry/__init__.py
A hurry.file/trunk/src/hurry/file/
A hurry.file/trunk/src/hurry/file/README.txt
A hurry.file/trunk/src/hurry/file/__init__.py
A hurry.file/trunk/src/hurry/file/browser/
A hurry.file/trunk/src/hurry/file/browser/__init__.py
A hurry.file/trunk/src/hurry/file/browser/file.txt
A hurry.file/trunk/src/hurry/file/browser/tests.py
A hurry.file/trunk/src/hurry/file/browser/widget.py
A hurry.file/trunk/src/hurry/file/configure.zcml
A hurry.file/trunk/src/hurry/file/file.py
A hurry.file/trunk/src/hurry/file/interfaces.py
A hurry.file/trunk/src/hurry/file/schema.py
A hurry.file/trunk/src/hurry/file/tests.py
-=-
Added: hurry.file/trunk/CHANGES.txt
===================================================================
--- hurry.file/trunk/CHANGES.txt (rev 0)
+++ hurry.file/trunk/CHANGES.txt 2008-09-16 21:47:14 UTC (rev 91194)
@@ -0,0 +1,64 @@
+hurry.file changes
+==================
+
+1.2 (unreleased)
+----------------
+
+* ...
+
+1.1 (2008-08-07)
+----------------
+
+* Add in a buildout.cfg that installs the test runner.
+
+* List dependencies in setup.py.
+
+* Rely on zope.session instead of zope.app.session to stop deprecation
+ warnings.
+
+* Add long description in setup.py based on README.txt and file.txt
+ doctests, and CHANGES.txt.
+
+1.0 (2006-10-25)
+----------------
+
+* Support for Tramline (fast file uploads/downloads) through
+ IFileRetrieval. By default, nothing changes.
+
+ If a subclass of TramlineFileRetrievalBase is registered as a
+ IFileRetrieval utility, hurry.file becomes Tramline aware. If files
+ are created manually, they can be created through the
+ createHurryFile function, or the 'createFile' method of the
+ IFileRetrieval service. This will take care of storing the file in
+ the right place.
+
+ Tramline can be found here: http://codespeak.net/svn/rr/tramline/trunk
+
+0.9.3 (2006-10-23)
+------------------
+
+* Send tramline_ok header back when redisplaying widget, in case we're
+ working with tramline.
+
+0.9.2 (2006-09-28)
+------------------
+
+* Zope 3.3 has a change in the way it deals with file name encoding
+ which broke hurry.file. This includes a workaround.
+
+0.9.1 (2006-09-22)
+------------------
+
+* first cheeseshop release.
+
+0.9 (2006-06-15)
+----------------
+
+* separation from general hurry package into hurry.file
+
+* eggification
+
+0.8 (2006-05-01)
+----------------
+
+Initial public release.
Added: hurry.file/trunk/CREDITS.txt
===================================================================
--- hurry.file/trunk/CREDITS.txt (rev 0)
+++ hurry.file/trunk/CREDITS.txt 2008-09-16 21:47:14 UTC (rev 91194)
@@ -0,0 +1,11 @@
+Credits
+-------
+
+* Martijn Faassen - initial and main developer
+
+* Daniel Nouri - bugfixes
+
+* Jan-Wijbrand Kolman - suggestions and feedback
+
+The hurry.file library for Zope 3 was developed at Infrae
+(http://www.infrae.com).
Added: hurry.file/trunk/README.txt
===================================================================
--- hurry.file/trunk/README.txt (rev 0)
+++ hurry.file/trunk/README.txt 2008-09-16 21:47:14 UTC (rev 91194)
@@ -0,0 +1,3 @@
+hurry.file - advanced file widget which tries its best to behave like
+ other widgets. See the doctest in
+ src/hurry/file/browser/file.txt for some documentation.
Added: hurry.file/trunk/ZopePublicLicense.txt
===================================================================
--- hurry.file/trunk/ZopePublicLicense.txt (rev 0)
+++ hurry.file/trunk/ZopePublicLicense.txt 2008-09-16 21:47:14 UTC (rev 91194)
@@ -0,0 +1,54 @@
+Zope Public License (ZPL) Version 2.1
+-------------------------------------
+
+A copyright notice accompanies this license document that
+identifies the copyright holders.
+
+This license has been certified as open source. It has also
+been designated as GPL compatible by the Free Software
+Foundation (FSF).
+
+Redistribution and use in source and binary forms, with or
+without modification, are permitted provided that the
+following conditions are met:
+
+1. Redistributions in source code must retain the
+ accompanying copyright notice, this list of conditions,
+ and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the accompanying
+ copyright notice, this list of conditions, and the
+ following disclaimer in the documentation and/or other
+ materials provided with the distribution.
+
+3. Names of the copyright holders must not be used to
+ endorse or promote products derived from this software
+ without prior written permission from the copyright
+ holders.
+
+4. The right to distribute this software or to use it for
+ any purpose does not give you the right to use
+ Servicemarks (sm) or Trademarks (tm) of the copyright
+ holders. Use of them is covered by separate agreement
+ with the copyright holders.
+
+5. If any files are modified, you must cause the modified
+ files to carry prominent notices stating that you changed
+ the files and the date of any change.
+
+Disclaimer
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS''
+ AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
+ NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
+ AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
+ NO EVENT SHALL THE COPYRIGHT HOLDERS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+ DAMAGE.
Added: hurry.file/trunk/bootstrap.py
===================================================================
--- hurry.file/trunk/bootstrap.py (rev 0)
+++ hurry.file/trunk/bootstrap.py 2008-09-16 21:47:14 UTC (rev 91194)
@@ -0,0 +1,55 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Bootstrap a buildout-based project
+
+Simply run this script in a directory containing a buildout.cfg.
+The script accepts buildout command-line options, so you can
+use the -c option to specify an alternate configuration file.
+
+$Id: bootstrap.py 75593 2007-05-06 21:11:27Z jim $
+"""
+
+import os, shutil, sys, tempfile, urllib2
+
+tmpeggs = tempfile.mkdtemp()
+
+try:
+ import pkg_resources
+except ImportError:
+ ez = {}
+ exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py'
+ ).read() in ez
+ ez['use_setuptools'](to_dir=tmpeggs, download_delay=0)
+
+ import pkg_resources
+
+cmd = 'from setuptools.command.easy_install import main; main()'
+if sys.platform == 'win32':
+ cmd = '"%s"' % cmd # work around spawn lamosity on windows
+
+ws = pkg_resources.working_set
+assert os.spawnle(
+ os.P_WAIT, sys.executable, sys.executable,
+ '-c', cmd, '-mqNxd', tmpeggs, 'zc.buildout',
+ dict(os.environ,
+ PYTHONPATH=
+ ws.find(pkg_resources.Requirement.parse('setuptools')).location
+ ),
+ ) == 0
+
+ws.add_entry(tmpeggs)
+ws.require('zc.buildout')
+import zc.buildout.buildout
+zc.buildout.buildout.main(sys.argv[1:] + ['bootstrap'])
+shutil.rmtree(tmpeggs)
Added: hurry.file/trunk/buildout.cfg
===================================================================
--- hurry.file/trunk/buildout.cfg (rev 0)
+++ hurry.file/trunk/buildout.cfg 2008-09-16 21:47:14 UTC (rev 91194)
@@ -0,0 +1,9 @@
+[buildout]
+develop = .
+parts = test
+newest = false
+
+[test]
+recipe = zc.recipe.testrunner
+eggs = hurry.file
+defaults = ['--tests-pattern', '^f?tests$', '-v']
Added: hurry.file/trunk/setup.py
===================================================================
--- hurry.file/trunk/setup.py (rev 0)
+++ hurry.file/trunk/setup.py 2008-09-16 21:47:14 UTC (rev 91194)
@@ -0,0 +1,53 @@
+from setuptools import setup, find_packages
+import os
+
+def read(*rnames):
+ return open(os.path.join(os.path.dirname(__file__), *rnames)).read()
+
+long_description = (
+ read('src/hurry/file/README.txt')
+ + '\n' +
+ read('src/hurry/file/browser/file.txt')
+ + '\n' +
+ read('CHANGES.txt')
+ )
+
+setup(
+ name="hurry.file",
+ version="1.2dev",
+ description="""\
+hurry.file is an advanced Zope 3 file widget which tries its best to behave
+like other widgets, even when the form is redisplayed due to a validation
+error. It also has built-in support for fast Apache-based file uploads
+and downloads through Tramline.
+""",
+ long_description=long_description,
+ classifiers=[
+ "Programming Language :: Python",
+ "Framework :: Zope3",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+ ],
+ keywords='zope zope3',
+ author='Martijn Faassen, Infrae',
+ author_email='faassen at startifact.com',
+ url='',
+ license='ZPL 2.1',
+ packages=find_packages('src'),
+ package_dir= {'':'src'},
+ namespace_packages=['hurry'],
+ include_package_data=True,
+ zip_safe=False,
+ install_requires=[
+ 'setuptools',
+ 'ZODB3',
+ 'zope.interface',
+ 'zope.schema',
+ 'zope.component',
+ 'zope.testing',
+ 'zope.publisher',
+ 'zope.app.form',
+ 'zope.session',
+ 'zope.app.testing',
+ 'zope.app.container',
+ ],
+ )
Added: hurry.file/trunk/src/hurry/__init__.py
===================================================================
--- hurry.file/trunk/src/hurry/__init__.py (rev 0)
+++ hurry.file/trunk/src/hurry/__init__.py 2008-09-16 21:47:14 UTC (rev 91194)
@@ -0,0 +1,7 @@
+# this is a namespace package
+try:
+ import pkg_resources
+ pkg_resources.declare_namespace(__name__)
+except ImportError:
+ import pkgutil
+ __path__ = pkgutil.extend_path(__path__, __name__)
Added: hurry.file/trunk/src/hurry/file/README.txt
===================================================================
--- hurry.file/trunk/src/hurry/file/README.txt (rev 0)
+++ hurry.file/trunk/src/hurry/file/README.txt 2008-09-16 21:47:14 UTC (rev 91194)
@@ -0,0 +1,124 @@
+hurry.file fields
+=================
+
+The file widget is built on top of the HurryFile object::
+
+ >>> from hurry.file import HurryFile
+ >>> file = HurryFile('foo.txt', 'mydata')
+ >>> file.filename
+ 'foo.txt'
+ >>> file.data
+ 'mydata'
+ >>> f = file.file
+ >>> f.read()
+ 'mydata'
+
+We can also create HurryFile objects from file-like objects::
+
+ >>> from StringIO import StringIO
+ >>> from zope import component
+ >>> from hurry.file.interfaces import IFileRetrieval
+ >>> fileretrieval = component.getUtility(IFileRetrieval)
+ >>> file = fileretrieval.createFile('bar.txt', StringIO('test data'))
+ >>> file.filename
+ 'bar.txt'
+ >>> file.data
+ 'test data'
+ >>> f = file.file
+ >>> f.read()
+ 'test data'
+
+This does exactly the same, but may be easier to use::
+
+ >>> from hurry.file import createHurryFile
+ >>> file = createHurryFile('test2.txt', StringIO('another test file'))
+ >>> file.filename
+ 'test2.txt'
+
+The HurryFile object normally stores the file data using ZODB
+persistence. Files can however also be stored by tramline. If
+tramline is installed in Apache, the Tramline takes care of generating
+ids for files and storing the file on the filesystem directly. The ids
+are then passed as file data to be stored in the ZODB.
+
+Let's first enable tramline.
+
+The tramline directory structure is a directory with two subdirectories,
+one called 'repository' and the other called 'upload'::
+
+ >>> import tempfile, os
+ >>> dirpath = tempfile.mkdtemp()
+ >>> repositorypath = os.path.join(dirpath, 'repository')
+ >>> uploadpath = os.path.join(dirpath, 'upload')
+ >>> os.mkdir(repositorypath)
+ >>> os.mkdir(uploadpath)
+
+We create a TramlineFileRetrieval object knowing about this directory,
+and register it as a utility::
+
+ >>> from hurry.file.file import TramlineFileRetrievalBase
+ >>> class TramlineFileRetrieval(TramlineFileRetrievalBase):
+ ... def getTramlinePath(self):
+ ... return dirpath
+ >>> retrieval = TramlineFileRetrieval()
+ >>> component.provideUtility(retrieval, IFileRetrieval)
+
+Now let's store a file the way tramline would during upload::
+
+ >>> f = open(os.path.join(repositorypath, '1'), 'wb')
+ >>> f.write('test data')
+ >>> f.close()
+
+The file with the data '1' will now be created::
+
+ >>> file = HurryFile('foo.txt', '1')
+
+The data is now '1'::
+
+ >>> file.data
+ '1'
+
+Retrieving the file results in the real file::
+
+ >>> f = file.file
+ >>> f.read()
+ 'test data'
+
+It should be possible to create Hurry File objects that are stored in
+the directory structure directly::
+
+ >>> file = retrieval.createFile('test.txt', StringIO('my test data'))
+ >>> file.filename
+ 'test.txt'
+
+We get an id for the data now::
+
+ >>> file.data != 'my test data'
+ True
+
+And we can retrieve the file itself::
+
+ >>> f = file.file
+ >>> f.read()
+ 'my test data'
+
+Now let's disable tramline in our utility::
+
+ >>> class TramlineFileRetrieval(TramlineFileRetrievalBase):
+ ... def getTramlinePath(self):
+ ... return dirpath
+ ... def isTramlineEnabled(self):
+ ... return False
+ >>> component.provideUtility(TramlineFileRetrieval(), IFileRetrieval)
+
+We expect the same behavior as when tramline is not installed::
+
+ >>> file = HurryFile('foo.txt', 'data')
+ >>> f = file.file
+ >>> f.read()
+ 'data'
+
+Clean up::
+
+ >>> import shutil
+ >>> shutil.rmtree(dirpath)
Added: hurry.file/trunk/src/hurry/file/__init__.py
===================================================================
--- hurry.file/trunk/src/hurry/file/__init__.py (rev 0)
+++ hurry.file/trunk/src/hurry/file/__init__.py 2008-09-16 21:47:14 UTC (rev 91194)
@@ -0,0 +1,2 @@
+# this is a package
+from file import HurryFile, createHurryFile
Added: hurry.file/trunk/src/hurry/file/browser/__init__.py
===================================================================
--- hurry.file/trunk/src/hurry/file/browser/__init__.py (rev 0)
+++ hurry.file/trunk/src/hurry/file/browser/__init__.py 2008-09-16 21:47:14 UTC (rev 91194)
@@ -0,0 +1,2 @@
+from widget import DownloadWidget, SessionFileWidget, EncodingFileWidget,\
+ EncodingFileUploadDownloadWidget, SessionFileUploadDownloadWidget
Added: hurry.file/trunk/src/hurry/file/browser/file.txt
===================================================================
--- hurry.file/trunk/src/hurry/file/browser/file.txt (rev 0)
+++ hurry.file/trunk/src/hurry/file/browser/file.txt 2008-09-16 21:47:14 UTC (rev 91194)
@@ -0,0 +1,179 @@
+hurry.file widgets
+==================
+
+This is an infrastructure to create a file widget that behaves as much
+as possible like a normal text widget in formlib. Normally a file
+widget loses its file data when a form is re-presented for reasons of
+failing form validation. A ``hurry.file`` widget retains the file, for
+example by storing it in a session.
+
+In order to do this, we have a special way to store file data along with
+its filename::
+
+ >>> from hurry.file import HurryFile
+ >>> some_file = HurryFile('foo.txt', 'the contents')
+ >>> some_file.filename
+ 'foo.txt'
+ >>> some_file.data
+ 'the contents'
+
+We can provide a download widget. In this case, there's nothing
+to download::
+
+ >>> from hurry.file.browser import DownloadWidget
+ >>> from hurry.file.schema import File
+ >>> from zope.publisher.browser import TestRequest
+ >>> field = File(__name__='foo', title=u'Foo')
+ >>> field = field.bind(None)
+ >>> request = TestRequest()
+ >>> widget = DownloadWidget(field, request)
+ >>> widget()
+ u'<div>Download not available</div>'
+
+Even if there were data in the request, there'd be nothing to download::
+
+ >>> from zope.publisher.browser import FileUpload
+ >>> request = TestRequest(form={'field.foo': FileUpload(some_file)})
+ >>> widget = DownloadWidget(field, request)
+ >>> widget()
+ u'<div>Download not available</div>'
+
+Now set a value::
+
+ >>> widget.setRenderedValue(some_file)
+ >>> widget()
+ u'<a href="foo.txt">foo.txt</a>'
+
+Now on to an edit widget. First the case in an add form with no
+data already available, and no data in request::
+
+ >>> from hurry.file.browser import EncodingFileWidget
+ >>> field = File(__name__='foo', title=u'Foo', required=False)
+ >>> field = field.bind(None)
+ >>> request = TestRequest()
+ >>> widget = EncodingFileWidget(field, request)
+
+ >>> def normalize(s):
+ ... return '\n '.join(filter(None, s.split(' ')))
+
+ >>> print normalize(widget())
+ <input
+ class="fileType"
+ id="field.foo"
+ name="field.foo"
+ size="20"
+ type="file"
+ />
+
+Now let's try a situation where data is available in the request, but
+it's an empty string for the file::
+
+ >>> request = TestRequest(form={'field.foo': u''})
+ >>> widget = EncodingFileWidget(field, request)
+
+ >>> def normalize(s):
+ ... return '\n '.join(filter(None, s.split(' ')))
+
+ >>> print normalize(widget())
+ <input
+ class="fileType"
+ id="field.foo"
+ name="field.foo"
+ size="20"
+ type="file"
+ />
+
+Now let's render again when there's already available data. What should show
+up is an extra, hidden field which contains the file_id::
+
+ >>> widget.setRenderedValue(some_file)
+ >>> print normalize(widget())
+ <input
+ class="fileType"
+ id="field.foo"
+ name="field.foo"
+ size="20"
+ type="file"
+ />
+ (foo.txt)<input
+ class="hiddenType"
+ id="field.foo.file_id"
+ name="field.foo.file_id"
+ type="hidden"
+ value="Zm9vLnR4dAp0aGUgY29udGVudHM="
+ />
+
+Now let's render again, this time with file data available in the request
+instead. The same should happen::
+
+ >>> request = TestRequest(form={'field.foo': FileUpload(some_file)})
+ >>> widget = EncodingFileWidget(field, request)
+ >>> print normalize(widget())
+ <input
+ class="fileType"
+ id="field.foo"
+ name="field.foo"
+ size="20"
+ type="file"
+ />
+ (foo.txt)<input
+ class="hiddenType"
+ id="field.foo.file_id"
+ name="field.foo.file_id"
+ type="hidden"
+ value="Zm9vLnR4dAp0aGUgY29udGVudHM="
+ />
+
+Now let's render again, this time not with file data available in the
+request, but an id. Again, we should see the same::
+
+ >>> request = TestRequest(form={'field.foo.file_id':
+ ... 'Zm9vLnR4dAp0aGUgY29udGVudHM='})
+ >>> widget = EncodingFileWidget(field, request)
+ >>> print normalize(widget())
+ <input
+ class="fileType"
+ id="field.foo"
+ name="field.foo"
+ size="20"
+ type="file"
+ />
+ (foo.txt)<input
+ class="hiddenType"
+ id="field.foo.file_id"
+ name="field.foo.file_id"
+ type="hidden"
+ value="Zm9vLnR4dAp0aGUgY29udGVudHM="
+ />
+
+If there is both file data and an id, something else happens. First, let's
+prepare some new file::
+
+ >>> another_file = HurryFile('bar.txt', 'bar contents')
+
+We happen to know, due to the implementation of EncodingFileWidget,
+that the file_id is going to be "YmFyLnR4dApiYXIgY29udGVudHM=". Let's
+make a request with the original id, but a new file upload::
+
+ >>> request = TestRequest(form={'field.foo': FileUpload(another_file),
+ ... 'field.foo.file_id':
+ ... 'Zm9vLnR4dAp0aGUgY29udGVudHM='})
+
+We expect the new file to be the one that's uploaded::
+
+ >>> widget = EncodingFileWidget(field, request)
+ >>> print normalize(widget())
+ <input
+ class="fileType"
+ id="field.foo"
+ name="field.foo"
+ size="20"
+ type="file"
+ />
+ (bar.txt)<input
+ class="hiddenType"
+ id="field.foo.file_id"
+ name="field.foo.file_id"
+ type="hidden"
+ value="YmFyLnR4dApiYXIgY29udGVudHM="
+ />
Added: hurry.file/trunk/src/hurry/file/browser/tests.py
===================================================================
--- hurry.file/trunk/src/hurry/file/browser/tests.py (rev 0)
+++ hurry.file/trunk/src/hurry/file/browser/tests.py 2008-09-16 21:47:14 UTC (rev 91194)
@@ -0,0 +1,25 @@
+import unittest
+
+from zope import component
+from zope.testing import doctest
+from zope.app.testing import placelesssetup
+from zope import component
+
+from hurry.file.file import IdFileRetrieval
+from hurry.file.interfaces import IFileRetrieval
+
+def fileSetUp(doctest):
+ placelesssetup.setUp()
+ component.provideUtility(IdFileRetrieval(), IFileRetrieval)
+
+def test_suite():
+ return unittest.TestSuite((
+ doctest.DocFileSuite(
+ 'file.txt',
+ setUp=fileSetUp, tearDown=placelesssetup.tearDown,
+ ),
+ ))
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='test_suite')
+
Added: hurry.file/trunk/src/hurry/file/browser/widget.py
===================================================================
--- hurry.file/trunk/src/hurry/file/browser/widget.py (rev 0)
+++ hurry.file/trunk/src/hurry/file/browser/widget.py 2008-09-16 21:47:14 UTC (rev 91194)
@@ -0,0 +1,205 @@
+import random, sys
+from StringIO import StringIO
+
+from zope.interface import implements
+from zope.publisher.browser import FileUpload
+
+from zope.app.form.browser import DisplayWidget
+from zope.app.form.interfaces import IDisplayWidget
+from zope.app.form.browser.widget import renderElement
+from zope.app.form.interfaces import ConversionError
+from zope.app.form.browser.textwidgets import escape
+from zope.session.interfaces import ISession
+from zope.app.form.browser import TextWidget
+
+from hurry.file.file import HurryFile
+
+class DownloadWidget(DisplayWidget):
+ """Display widget for file download.
+ """
+ implements(IDisplayWidget)
+
+ required = False
+
+ def __call__(self):
+ if self._renderedValueSet():
+ value = self._data
+ else:
+ value = self.context.default
+ if value == self.context.missing_value:
+ return renderElement(
+ u'div',
+ contents=u'Download not available')
+ filename = escape(value.filename)
+ return renderElement(
+ u'a',
+ href=filename,
+ contents=filename)
+
+class FakeFieldStorage:
+ def __init__(self, filename, data):
+ self.filename = filename
+ self.file = StringIO(data)
+ self.headers = {}
+
+class FileWidgetBase(TextWidget):
+ type = 'file'
+ _missing = None
+
+ def __call__(self):
+ value = self._getFormValue()
+ if value:
+ file_id = self._setFile(value)
+ else:
+ file_id = None
+
+ displayMaxWidth = self.displayMaxWidth or 0
+ if displayMaxWidth > 0:
+ result = renderElement(
+ self.tag,
+ type=self.type,
+ name=self.name,
+ id=self.name,
+ cssClass=self.cssClass,
+ size=self.displayWidth,
+ maxlength=displayMaxWidth,
+ extra=self.extra)
+ else:
+ result = renderElement(
+ self.tag,
+ type=self.type,
+ name=self.name,
+ id=self.name,
+ cssClass=self.cssClass,
+ size=self.displayWidth,
+ extra=self.extra)
+
+ # if there was data in the input, pass along the data id
+ if file_id is not None:
+ # tell tramline to store this file (if tramline is in use)
+ self.request.response.addHeader('tramline_ok', 'OK')
+ if value:
+ result += ' (%s)' % value.filename
+ result += renderElement(
+ 'input',
+ type='hidden',
+ name=self.name + '.file_id',
+ id=self.name + '.file_id',
+ value=file_id,
+ )
+ return result
+
+ def hasInput(self):
+ return (self.name in self.request.form or
+ self.name + '.file_id' in self.request.form)
+
+ def _getFormInput(self):
+ return (self.request.get(self.name),
+ self.request.get(self.name + '.file_id'))
+
+ def _toFormValue(self, value):
+ if value == self.context.missing_value:
+ return self._missing
+ return FileUpload(FakeFieldStorage(value.filename.encode('UTF-8'),
+ value.data))
+
+ def _toFieldValue(self, (input, file_id)):
+ # we got no file upload input
+ if not input:
+ # if we got a file_id, then retrieve file and return it
+ if file_id:
+ return self._retrieveFile(file_id)
+ # no file upload input nor file id, so return missing value
+ return self.context.missing_value
+ # read in file from input
+ try:
+ seek = input.seek
+ read = input.read
+ except AttributeError, e:
+ raise ConversionError('Form input is not a file object', e)
+
+ seek(0)
+ data = read()
+
+ if data:
+ return HurryFile(input.filename, data)
+ else:
+ return self.context.missing_value
+
+ def _setFile(self, file):
+ """Store away uploaded file (FileUpload object).
+
+ Returns file_id identifying file.
+ """
+ # if there was no file input and there was a file_id already in the
+ # input, reuse this for next request
+ if not self.request.get(self.name):
+ file_id = self.request.get(self.name + '.file_id')
+ if file_id is not None:
+ return file_id
+ # otherwise, stuff filedata away in session, making a new file_id
+ if file == self.context.missing_value:
+ return None
+ return self._storeFile(file)
+
+ def _storeFile(self, file_upload):
+ """Store a file_upload away. Return unique file id.
+ """
+ raise NotImplementedError
+
+ def _retrieveFile(self, file_id):
+ """Retrieve a file. This returns a HurryFile, *not* a FileUpload.
+ """
+ raise NotImplementedError
+
+class EncodingFileWidget(FileWidgetBase):
+ """Stuff actual file data away in form, encoded.
+ """
+ def _storeFile(self, file_upload):
+ data = '%s\n%s' % (file_upload.filename, file_upload.read())
+ return data.encode('base64')[:-1]
+
+ def _retrieveFile(self, file_id):
+ data = file_id.decode('base64')
+ filename, filedata = data.split('\n', 1)
+ return HurryFile(filename, filedata)
+
+class SessionFileWidget(FileWidgetBase):
+ """Stuff file in session.
+ """
+ def _storeFile(self, file_upload):
+ """Store a file away. Return unique file id.
+ """
+ session = ISession(self.request)
+ while True:
+ file_id = random.randrange(sys.maxint)
+ session_id = 'session_file_widget.%s' % file_id
+ session_data = session[session_id]
+ if not session_data.has_key('data'):
+ break
+ session_data['data'] = {'filename': file_upload.filename,
+ 'data': file_upload.read()}
+ return file_id
+
+ def _retrieveFile(self, file_id):
+ session = ISession(self.request)
+ session_data = session['session_file_widget.%s' % file_id]['data']
+ return HurryFile(session_data['filename'], session_data['data'])
+
+class EncodingFileUploadDownloadWidget(EncodingFileWidget, DownloadWidget):
+ def __call__(self):
+ # first render the normal upload information
+ result = EncodingFileWidget.__call__(self)
+ # now show the download link
+ result += renderElement('br')
+ result += DownloadWidget.__call__(self)
+ return result
+
+class SessionFileUploadDownloadWidget(SessionFileWidget, DownloadWidget):
+ def __call__(self):
+ # first render the normal upload information
+ result = SessionFileWidget.__call__(self)
+ # now show the download link
+ result += renderElement('br')
+ result += DownloadWidget.__call__(self)
+ return result
Added: hurry.file/trunk/src/hurry/file/configure.zcml
===================================================================
--- hurry.file/trunk/src/hurry/file/configure.zcml (rev 0)
+++ hurry.file/trunk/src/hurry/file/configure.zcml 2008-09-16 21:47:14 UTC (rev 91194)
@@ -0,0 +1,11 @@
+<configure
+ xmlns="http://namespaces.zope.org/zope"
+ >
+
+ <utility provides=".interfaces.IFileRetrieval"
+ factory=".file.IdFileRetrieval" />
+
+<!-- the widget is not yet the default widget for FileUpload; use
+ formlib's custom_widget system to use it. This may change -->
+
+</configure>
Added: hurry.file/trunk/src/hurry/file/file.py
===================================================================
--- hurry.file/trunk/src/hurry/file/file.py (rev 0)
+++ hurry.file/trunk/src/hurry/file/file.py 2008-09-16 21:47:14 UTC (rev 91194)
@@ -0,0 +1,107 @@
+import sys, os, random, threading
+from StringIO import StringIO
+from persistent import Persistent
+from zope.interface import implements
+from hurry.file import interfaces
+from zope import component
+from zope.app.container.contained import Contained
+
+class HurryFile(Persistent):
+ implements(interfaces.IHurryFile)
+
+ def __init__(self, filename, data):
+ self.filename = filename
+ self.data = data
+ self.headers = {}
+
+ def _get_file(self):
+ storage = component.getUtility(interfaces.IFileRetrieval)
+ return storage.getFile(self.data)
+
+ file = property(_get_file)
+
+ def __eq__(self, other):
+ try:
+ return (self.filename == other.filename and
+ self.data == other.data)
+ except AttributeError:
+ return False
+
+ def __neq__(self, other):
+ try:
+ return (self.filename != other.filename or
+ self.data != other.data)
+ except AttributeError:
+ return True
+
+def createHurryFile(filename, f):
+ retrieval = component.getUtility(interfaces.IFileRetrieval)
+ return retrieval.createFile(filename, f)
+
+class IdFileRetrieval(Persistent, Contained):
+ """Very basic implementation of FileRetrieval.
+
+ This implementation just returns a File object for the data.
+ """
+ implements(interfaces.IFileRetrieval)
+
+ def getFile(self, data):
+ return StringIO(data)
+
+ def createFile(self, filename, f):
+ return HurryFile(filename, f.read())
+
+class TramlineFileRetrievalBase(Persistent, Contained):
+ """File retrieval for tramline (base class).
+ """
+ implements(interfaces.IFileRetrieval)
+
+ def getTramlinePath(self):
+ raise NotImplementedError
+
+ def isTramlineEnabled(self):
+ return True
+
+ def getFile(self, data):
+ # tramline is disabled, so give fall-back behavior for testing
+ # without tramline
+ if not self.isTramlineEnabled():
+ return StringIO(data)
+ # we need to retrieve the actual file from the filesystem
+ # it could be either a permanently stored file in the repository,
+ # or, if that isn't available, potentially a file in the upload
+ # directory
+ path = self.getTramlinePath()
+ if not path:
+ raise ValueError("No tramline path configured")
+ repository_path = os.path.join(path, 'repository', data)
+ try:
+ f = open(repository_path, 'rb')
+ except IOError:
+ upload_path = os.path.join(path, 'upload', data)
+ f = open(upload_path, 'rb')
+ return f
+
+ def createFile(self, filename, f):
+ repository_path = os.path.join(self.getTramlinePath(),
+ 'repository')
+ # XXX try to make this thread-safe, but it's not 100% ZEO safe..
+ lock = threading.Lock()
+ lock.acquire()
+ try:
+ while True:
+ file_id = str(random.randrange(sys.maxint))
+ if os.path.exists(os.path.join(repository_path,
+ file_id)):
+ continue # try again
+ break
+ path = os.path.join(repository_path, file_id)
+ of = open(path, 'wb')
+ finally:
+ lock.release()
+
+ # XXX this can be made more efficient
+ of.write(f.read())
+ of.close()
+
+ return HurryFile(filename, file_id)
Added: hurry.file/trunk/src/hurry/file/interfaces.py
===================================================================
--- hurry.file/trunk/src/hurry/file/interfaces.py (rev 0)
+++ hurry.file/trunk/src/hurry/file/interfaces.py 2008-09-16 21:47:14 UTC (rev 91194)
@@ -0,0 +1,21 @@
+from zope.interface import Interface, Attribute
+from zope.schema.interfaces import IField
+from zope.schema import TextLine, Bytes
+
+class IFile(IField):
+ u"""File field."""
+
+class IHurryFile(Interface):
+ filename = TextLine(title=u'Filename of file')
+ data = Bytes(title=u'Data in file')
+ file = Attribute('File-like object with data')
+ headers = Attribute('Headers associated with file')
+
+class IFileRetrieval(Interface):
+ def getFile(data):
+ """Get a file object for file data.
+ """
+
+ def createFile(filename, f):
+ """Given a file object, create a HurryFile with that data in it.
+ """
Added: hurry.file/trunk/src/hurry/file/schema.py
===================================================================
--- hurry.file/trunk/src/hurry/file/schema.py (rev 0)
+++ hurry.file/trunk/src/hurry/file/schema.py 2008-09-16 21:47:14 UTC (rev 91194)
@@ -0,0 +1,12 @@
+from zope.interface import implements
+from zope.schema import Field
+
+from hurry.file.interfaces import IFile
+from hurry.file.file import HurryFile
+
+class File(Field):
+ __doc__ = IFile.__doc__
+
+ implements(IFile)
+
+ _type = HurryFile
Added: hurry.file/trunk/src/hurry/file/tests.py
===================================================================
--- hurry.file/trunk/src/hurry/file/tests.py (rev 0)
+++ hurry.file/trunk/src/hurry/file/tests.py 2008-09-16 21:47:14 UTC (rev 91194)
@@ -0,0 +1,24 @@
+import unittest
+
+from zope.testing import doctest
+from zope.app.testing import placelesssetup
+from zope import component
+
+from hurry.file.file import IdFileRetrieval
+from hurry.file.interfaces import IFileRetrieval
+
+def fileSetUp(doctest):
+ placelesssetup.setUp()
+ component.provideUtility(IdFileRetrieval(), IFileRetrieval)
+
+def test_suite():
+ return unittest.TestSuite((
+ doctest.DocFileSuite(
+ 'README.txt',
+ setUp=fileSetUp, tearDown=placelesssetup.tearDown,
+ ),
+ ))
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='test_suite')
+
More information about the Checkins
mailing list