[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