zope.file-0.6.2/0000775000175000017500000000000011763154606013031 5ustar uliuli00000000000000zope.file-0.6.2/setup.cfg0000664000175000017500000000007311763154606014652 0ustar uliuli00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 zope.file-0.6.2/src/0000775000175000017500000000000011763154606013620 5ustar uliuli00000000000000zope.file-0.6.2/src/zope/0000775000175000017500000000000011763154606014575 5ustar uliuli00000000000000zope.file-0.6.2/src/zope/file/0000775000175000017500000000000011763154606015514 5ustar uliuli00000000000000zope.file-0.6.2/src/zope/file/upload.py0000664000175000017500000001255611763153771017365 0ustar uliuli00000000000000############################################################################## # # Copyright (c) 2005 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. # ############################################################################## """Upload view for zope.file objects. """ __docformat__ = "reStructuredText" import re import zope.container.interfaces import zope.contenttype.parse import zope.lifecycleevent import zope.component import zope.event import zope.file.file import zope.formlib.form import zope.mimetype.event import zope.mimetype.interfaces import zope.schema import zope.security.proxy from zope import mimetype from zope.file.i18n import _ _nameFinder = re.compile(r'(.*[\\/:])?(.+)') def nameFinder(fileob): name = getattr(fileob, 'filename', None) if name is None: return None match = _nameFinder.match(name) if match is not None: match = match.group(2) return match class Upload(zope.formlib.form.AddForm): form_fields = zope.formlib.form.fields( zope.schema.Bytes( __name__="data", title=_("Upload data"), description=_("Upload file"), ), ) def create(self, data): ob = self._create_instance(data) f = self.request.form["form.data"] updateBlob(ob, f) self._name = nameFinder(f) return ob def add(self, ob): if self._name and self.context.nameAllowed(): nc = zope.container.interfaces.INameChooser( self.context.context) name = nc.chooseName(self._name, ob) self.context.contentName = name res = super(Upload, self).add(ob) # We need to slam on an interface based on the effective MIME type; # start by getting the IContentInfo for ob: ci = mimetype.interfaces.IContentInfo(ob, None) if (ci is not None) and ci.effectiveMimeType: # we might have *something* iface = zope.component.queryUtility( mimetype.interfaces.IContentTypeInterface, ci.effectiveMimeType) if iface is not None: mimetype.event.changeContentType(ob, iface) return res def _create_instance(self, data): return zope.file.file.File() class Reupload(zope.formlib.form.Form): form_fields = zope.formlib.form.Fields( zope.schema.Bytes( __name__="data", title=_("Upload data"), description=_("Upload file to replace the current data"), ), ) @zope.formlib.form.action(_("Edit")) def upload(self, action, data): context = self.context if "charset" in context.parameters: old_codec = zope.component.queryUtility( mimetype.interfaces.ICharsetCodec, context.parameters["charset"]) else: old_codec = None if data.get("data") is not None: updateBlob(context, self.request.form["form.data"]) # update the encoding, but only if the new content type is encoded encoded = mimetype.interfaces.IContentTypeEncoded.providedBy( context) if encoded and "charset" in context.parameters: codec = zope.component.queryUtility( mimetype.interfaces.ICharsetCodec, context.parameters["charset"]) if codec and getattr(old_codec, "name", None) != codec.name: # use the preferred charset for the new codec new_charset = zope.component.getUtility( mimetype.interfaces.ICodecPreferredCharset, codec.name) parameters = dict(context.parameters) parameters["charset"] = new_charset.name context.parameters = parameters # these subscribers generally expect an unproxied object. zope.event.notify( zope.lifecycleevent.ObjectModifiedEvent( zope.security.proxy.removeSecurityProxy(context))) def updateBlob(ob, input): # Bypass the widget machinery for now; we'd rather have a blob # widget that exposes the interesting information from the # upload. f = input # We need to seek back since the form machinery has already # read all the data :-( f.seek(0) data = f.read() contentType = f.headers.get("Content-Type") mimeTypeGetter = zope.component.getUtility( mimetype.interfaces.IMimeTypeGetter) mimeType = mimeTypeGetter(data=data, content_type=contentType, name=nameFinder(f)) if not mimeType: mimeType = "application/octet-stream" if contentType: major, minor, parameters = zope.contenttype.parse.parse( contentType) if "charset" in parameters: parameters["charset"] = parameters["charset"].lower() ob.mimeType = mimeType ob.parameters = parameters else: ob.mimeType = mimeType ob.parameters = {} w = ob.open("w") w.write(data) w.close() zope.file-0.6.2/src/zope/file/browser.py0000664000175000017500000000227511763153771017561 0ustar uliuli00000000000000############################################################################## # # Copyright (c) 2005 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. # ############################################################################## """Presentation adapters for zope.file. """ __docformat__ = "reStructuredText" import zope.size import zope.size.interfaces import zope.component import zope.file.interfaces import zope.interface class Sized(object): zope.interface.implements( zope.size.interfaces.ISized) zope.component.adapts( zope.file.interfaces.IFile) def __init__(self, context): self.context = context def sizeForSorting(self): return "byte", self.context.size def sizeForDisplay(self): return zope.size.byteDisplay(self.context.size) zope.file-0.6.2/src/zope/file/menus.zcml0000664000175000017500000000155511763153771017542 0ustar uliuli00000000000000 zope.file-0.6.2/src/zope/file/README.txt0000664000175000017500000001021011763153771017206 0ustar uliuli00000000000000=========== File Object =========== The `zope.file` package provides a content object used to store a file. The interface supports efficient upload and download. Let's create an instance: >>> from zope.file.file import File >>> f = File() The object provides a limited number of data attributes. The `mimeType` attribute is used to store the preferred MIME content-type value for the data: >>> f.mimeType >>> f.mimeType = "text/plain" >>> f.mimeType 'text/plain' >>> f.mimeType = "application/postscript" >>> f.mimeType 'application/postscript' The `parameters` attribute is a mapping used to store the content-type parameters. This is where encoding information can be found when applicable (and available): >>> f.parameters {} >>> f.parameters["charset"] = "us-ascii" >>> f.parameters["charset"] 'us-ascii' Both, `parameters` and `mimeType` can optionally also be set when creating a `File` object: >>> f2 = File(mimeType = "application/octet-stream", ... parameters = dict(charset = "utf-8")) >>> f2.mimeType 'application/octet-stream' >>> f2.parameters["charset"] 'utf-8' File objects also sport a `size` attribute that provides the number of bytes in the file: >>> f.size 0 The object supports efficient upload and download by providing all access to content data through accessor objects that provide (subsets of) Python's file API. A file that hasn't been written to is empty. We can get a reader by calling `open()`. Note that all blobs are binary, thus the mode always contains a 'b': >>> r = f.open("r") >>> r.mode 'rb' The `read()` method can be called with a non-negative integer argument to specify how many bytes to read, or with a negative or omitted argument to read to the end of the file: >>> r.read(10) '' >>> r.read() '' >>> r.read(-1) '' Once the accessor has been closed, we can no longer read from it: >>> r.close() >>> r.read() Traceback (most recent call last): ValueError: I/O operation on closed file We'll see that readers are more interesting once there's data in the file object. Data is added by using a writer, which is also created using the `open()` method on the file, but requesting a write file mode: >>> w = f.open("w") >>> w.mode 'wb' The `write()` method is used to add data to the file, but note that the data may be buffered in the writer: >>> w.write("some text ") >>> w.write("more text") The `flush()` method ensure that the data written so far is written to the file object: >>> w.flush() We need to close the file first before determining its file size >>> w.close() >>> f.size 19 We can now use a reader to see that the data has been written to the file: >>> w = f.open("w") >>> w.write('some text more text') >>> w.write(" still more") >>> w.close() >>> f.size 30 Now create a new reader and let's perform some seek operations. >>> r = f.open() The reader also has a `seek()` method that can be used to back up or skip forward in the data stream. Simply passing an offset argument, we see that the current position is moved to that offset from the start of the file: >>> r.seek(20) >>> r.read() 'still more' That's equivalent to passing 0 as the `whence` argument: >>> r.seek(20, 0) >>> r.read() 'still more' We can skip backward and forward relative to the current position by passing 1 for `whence`: >>> r.seek(-10, 1) >>> r.read(5) 'still' >>> r.seek(2, 1) >>> r.read() 'ore' We can skip to some position backward from the end of the file using the value 2 for `whence`: >>> r.seek(-10, 2) >>> r.read() 'still more' >>> r.seek(0) >>> r.seek(-4, 2) >>> r.read() 'more' >>> r.close() Attempting to write to a closed writer raises an exception: >>> w = f.open('w') >>> w.close() >>> w.write('foobar') Traceback (most recent call last): ValueError: I/O operation on closed file Similarly, using `seek()` or `tell()` on a closed reader raises an exception: >>> r.close() >>> r.seek(0) Traceback (most recent call last): ValueError: I/O operation on closed file >>> r.tell() Traceback (most recent call last): ValueError: I/O operation on closed file zope.file-0.6.2/src/zope/file/upload.txt0000664000175000017500000001215711763153771017551 0ustar uliuli00000000000000==================== Uploading a new file ==================== There's a simple view for uploading a new file. Let's try it: >>> from StringIO import StringIO >>> sio = StringIO("some text") >>> sio.filename = "plain.txt" >>> sio.headers = {"Content-Type": "text/plain; charset=utf-8", ... "Content-Disposition": 'attachment; filename="plain.txt"'} >>> print http(""" ... POST /@@+/zope.file.File HTTP/1.1 ... Authorization: Basic mgr:mgrpw ... """, form={"form.data": sio, ... "form.actions.add": "Add"}, handle_errors=False) HTTP/1.1 303 ... Now, let's request the download view of the file object and check the result: >>> print http(""" ... GET /plain.txt/@@download HTTP/1.1 ... Authorization: Basic mgr:mgrpw ... """, handle_errors=False) HTTP/1.1 200 Ok Content-Disposition: attachment; filename="plain.txt" Content-Length: 9 Content-Type: text/plain;charset=utf-8 some text We'll peek into the database to make sure the object implements the expected MIME type interface: >>> from zope.mimetype import types >>> ob = getRootFolder()["plain.txt"] >>> types.IContentTypeTextPlain.providedBy(ob) True We can upload new data into our file object as well: >>> sio = StringIO("new text") >>> sio.filename = "stuff.txt" >>> sio.headers = {"Content-Type": "text/plain; charset=utf-8", ... "Content-Disposition": 'attachment; filename="stuff.txt"'} >>> print http(""" ... POST /plain.txt/@@edit.html HTTP/1.1 ... Authorization: Basic mgr:mgrpw ... """, form={"form.data": sio, ... "form.actions.edit": "Edit"}, handle_errors=False) HTTP/1.1 200 ... Now, let's request the download view of the file object and check the result: >>> print http(""" ... GET /plain.txt/@@download HTTP/1.1 ... Authorization: Basic mgr:mgrpw ... """, handle_errors=False) HTTP/1.1 200 Ok Content-Disposition: attachment; filename="plain.txt" Content-Length: 8 Content-Type: text/plain;charset=utf-8 new text If we upload a file that has imprecise content type information (as we expect from browsers generally, and MSIE most significantly), we can see that the MIME type machinery will improve the information where possible: >>> sio = StringIO("\n" ... "...\n") >>> sio.filename = "simple.html" >>> sio.headers = { ... "Content-Type": "text/html; charset=utf-8", ... "Content-Disposition": 'attachment; filename="simple.html"', ... } >>> print http(""" ... POST /@@+/zope.file.File HTTP/1.1 ... Authorization: Basic mgr:mgrpw ... """, form={"form.data": sio, ... "form.actions.add": "Add"}, handle_errors=False) HTTP/1.1 303 ... Again, we'll request the download view of the file object and check the result: >>> print http(""" ... GET /simple.html/@@download HTTP/1.1 ... Authorization: Basic mgr:mgrpw ... """, handle_errors=False) HTTP/1.1 200 Ok Content-Disposition: attachment; filename="simple.html" Content-Length: 56 Content-Type: application/xhtml+xml;charset=utf-8 ... Further, if a browser is bad and sends a full path as the file name (as sometimes happens in many browsers, apparently), the name is correctly truncated and changed. >>> sio = StringIO("\n" ... "...\n") >>> sio.filename = r"C:\Documents and Settings\Joe\naughty name.html" >>> sio.headers = { ... "Content-Type": "text/html; charset=utf-8", ... "Content-Disposition": 'attachment; filename=%s' % sio.filename, ... } >>> print http(""" ... POST /@@+/zope.file.File HTTP/1.1 ... Authorization: Basic mgr:mgrpw ... """, form={"form.data": sio, ... "form.actions.add": "Add"}, handle_errors=False) HTTP/1.1 303 ... Again, we'll request the download view of the file object and check the result: >>> print http(""" ... GET /naughty%20name.html/@@download HTTP/1.1 ... Authorization: Basic mgr:mgrpw ... """, handle_errors=False) HTTP/1.1 200 Ok Content-Disposition: attachment; filename="naughty name.html" Content-Length: 56 Content-Type: application/xhtml+xml;charset=utf-8 ... In zope.file <= 0.5.0, a redundant ObjectCreatedEvent was fired in the Upload view. We'll demonstrate that this is no longer the case. >>> import zope.component >>> from zope.file.interfaces import IFile >>> from zope.lifecycleevent import IObjectCreatedEvent We'll register a subscriber for IObjectCreatedEvent that simply increments a counter. >>> count = 0 >>> def inc(*args): ... global count; count += 1 >>> zope.component.provideHandler(inc, (IFile, IObjectCreatedEvent)) >>> print http(""" ... POST /@@+/zope.file.File HTTP/1.1 ... Authorization: Basic mgr:mgrpw ... """, form={"form.data": sio, ... "form.actions.add": "Add"}, handle_errors=False) HTTP/1.1 303 ... The subscriber was called only once. >>> print count 1 zope.file-0.6.2/src/zope/file/browser.txt0000664000175000017500000000251011763153771017740 0ustar uliuli00000000000000===================== Presentation Adapters ===================== Object size ----------- The size of the file as presented in the contents view of a container is provided using an adapter implementing the `zope.size.interfaces.ISized` interface. Such an adapter is available for the file object. Let's do some imports and create a new file object: >>> from zope.file.file import File >>> from zope.file.browser import Sized >>> from zope.size.interfaces import ISized >>> f = File() >>> f.size 0 >>> s = Sized(f) >>> ISized.providedBy(s) True >>> s.sizeForSorting() ('byte', 0) >>> s.sizeForDisplay() u'0 KB' Let's add some content to the file: >>> w = f.open('w') >>> w.write("some text") >>> w.close() The sized adapter now reflects the updated size: >>> s.sizeForSorting() ('byte', 9) >>> s.sizeForDisplay() u'1 KB' Let's try again with a larger file size: >>> w = f.open('w') >>> w.write("x" * (1024*1024+10)) >>> w.close() >>> s.sizeForSorting() ('byte', 1048586) >>> m = s.sizeForDisplay() >>> m u'${size} MB' >>> m.mapping {'size': '1.00'} And still a bigger size: >>> w = f.open('w') >>> w.write("x" * 3*512*1024) >>> w.close() >>> s.sizeForSorting() ('byte', 1572864) >>> m = s.sizeForDisplay() >>> m u'${size} MB' >>> m.mapping {'size': '1.50'} zope.file-0.6.2/src/zope/file/adapters.txt0000664000175000017500000000274611763153771020073 0ustar uliuli00000000000000======== Adapters ======== The `zope.file` package provides some adapters to adapt file-like objects to `zope.filerepresentation` conform objects. There is a read-file adapter and a write-file adapter available. We start with a regular `File` object: >>> from zope.file.file import File >>> f = File(parameters=dict(charset='utf-8')) >>> f.open('w').write("hello") Now we can turn this file into a read-only file which we can read and whose size we can get: >>> from zope.filerepresentation.interfaces import IReadFile, IWriteFile >>> r = IReadFile(f) >>> r.read() 'hello' >>> r.size() 5 Writing to this read-only file is impossible, as the interface does not require it: >>> r.write("some more content") Traceback (most recent call last): AttributeError: 'ReadFileAdapter' object has no attribute 'write' With a write-file the opposite happens. We can write but not read: >>> w = IWriteFile(f) >>> w.write("some more content") >>> w.read() Traceback (most recent call last): AttributeError: 'WriteFileAdapter' object has no attribute 'read' The delivered adapters really comply with the promised interfaces: >>> from zope.interface.verify import verifyClass, verifyObject >>> from zope.file.adapters import ReadFileAdapter, WriteFileAdapter >>> verifyClass(IReadFile, ReadFileAdapter) True >>> verifyObject(IReadFile, r) True >>> verifyClass(IWriteFile, WriteFileAdapter) True >>> verifyObject(IWriteFile, w) True zope.file-0.6.2/src/zope/file/i18n.py0000664000175000017500000000142011763153771016644 0ustar uliuli00000000000000############################################################################## # # Copyright (c) 2005 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. # ############################################################################## """I18N support for zope.file. """ __docformat__ = "reStructuredText" import zope.i18nmessageid _ = zope.i18nmessageid.MessageFactory("zope.file") zope.file-0.6.2/src/zope/file/__init__.py0000664000175000017500000000004611763153771017627 0ustar uliuli00000000000000# This directory is a Python package. zope.file-0.6.2/src/zope/file/download.txt0000664000175000017500000001451011763153771020067 0ustar uliuli00000000000000======================== Downloading File Objects ======================== The file content type provides a view used to download the file, regardless of the browser's default behavior for the content type. This relies on browser support for the Content-Disposition header. The download support is provided by two distinct objects: A view that provides the download support using the information in the content object, and a result object that can be used to implement a file download by other views. The view can override the content-type or the filename suggested to the browser using the standard IResponse.setHeader method. Note that result objects are intended to be used once and then discarded. Let's start by creating a file object we can use to demonstrate the download support: >>> import transaction >>> from zope.file.file import File >>> f = File() >>> getRootFolder()['file'] = f >>> transaction.commit() Headers ------- Now, let's get the headers for this file. We use a utility function called ``getHeaders``: >>> from zope.file.download import getHeaders >>> headers = getHeaders(f, contentDisposition='attachment') Since there's no suggested download filename on the file, the Content-Disposition header doesn't specify one, but does indicate that the response body be treated as a file to save rather than to apply the default handler for the content type: >>> sorted(headers) [('Content-Disposition', 'attachment; filename="file"'), ('Content-Length', '0'), ('Content-Type', 'application/octet-stream')] Note that a default content type of 'application/octet-stream' is used. If the file object specifies a content type, that's used in the headers by default: >>> f.mimeType = "text/plain" >>> headers = getHeaders(f, contentDisposition='attachment') >>> sorted(headers) [('Content-Disposition', 'attachment; filename="file"'), ('Content-Length', '0'), ('Content-Type', 'text/plain')] Alternatively, a content type can be specified to ``getHeaders``: >>> headers = getHeaders(f, contentType="text/xml", ... contentDisposition='attachment') >>> sorted(headers) [('Content-Disposition', 'attachment; filename="file"'), ('Content-Length', '0'), ('Content-Type', 'text/xml')] The filename provided to the browser can be controlled similarly. If the content object provides one, it will be used by default: >>> headers = getHeaders(f, contentDisposition='attachment') >>> sorted(headers) [('Content-Disposition', 'attachment; filename="file"'), ('Content-Length', '0'), ('Content-Type', 'text/plain')] Providing an alternate name to ``getHeaders`` overrides the download name from the file: >>> headers = getHeaders(f, downloadName="foo.txt", ... contentDisposition='attachment') >>> sorted(headers) [('Content-Disposition', 'attachment; filename="foo.txt"'), ('Content-Length', '0'), ('Content-Type', 'text/plain')] The default Content-Disposition header can be overridden by providing an argument to ``getHeaders``: >>> headers = getHeaders(f, contentDisposition="inline") >>> sorted(headers) [('Content-Disposition', 'inline; filename="file"'), ('Content-Length', '0'), ('Content-Type', 'text/plain')] If the `contentDisposition` argument is not provided, none will be included in the headers: >>> headers = getHeaders(f) >>> sorted(headers) [('Content-Length', '0'), ('Content-Type', 'text/plain')] Body ---- We use DownloadResult to deliver the content to the browser. Since there's no data in this file, there are no body chunks: >>> transaction.commit() >>> from zope.file.download import DownloadResult >>> result = DownloadResult(f) >>> list(result) [] We still need to see how non-empty files are handled. Let's write some data to our file object: >>> w = f.open("w") >>> w.write("some text") >>> w.flush() >>> w.close() >>> transaction.commit() Now we can create a result object and see if we get the data we expect: >>> result = DownloadResult(f) >>> L = list(result) >>> "".join(L) 'some text' If the body content is really large, the iterator may provide more than one chunk of data: >>> w = f.open("w") >>> w.write("*" * 1024 * 1024) >>> w.flush() >>> w.close() >>> transaction.commit() >>> result = DownloadResult(f) >>> L = list(result) >>> len(L) > 1 True Once iteration over the body has completed, further iteration will not yield additional data: >>> list(result) [] The Download View ----------------- Now that we've seen the ``getHeaders`` function and the result object, let's take a look at the basic download view that uses them. We'll need to add a file object where we can get to it using a browser: >>> f = File() >>> f.mimeType = "text/plain" >>> w = f.open("w") >>> w.write("some text") >>> w.close() >>> transaction.commit() >>> getRootFolder()["abcdefg"] = f >>> transaction.commit() Now, let's request the download view of the file object and check the result: >>> print http(""" ... GET /abcdefg/@@download HTTP/1.1 ... Authorization: Basic mgr:mgrpw ... """, handle_errors=False) HTTP/1.1 200 Ok Content-Disposition: attachment; filename="abcdefg" Content-Length: 9 Content-Type: text/plain some text The Inline View --------------- In addition, it is sometimes useful to view the data inline instead of downloading it. A basic inline view is provided for this use case. Note that browsers may decide not to display the image when this view is used and there is not page that it's being loaded into: if this view is being referenced directly via the URL, the browser may show nothing: >>> print http(""" ... GET /abcdefg/@@inline HTTP/1.1 ... Authorization: Basic mgr:mgrpw ... """, handle_errors=False) HTTP/1.1 200 Ok Content-Disposition: inline; filename="abcdefg" Content-Length: 9 Content-Type: text/plain some text The Default Display View ------------------------ This view is similar to the download and inline views, but no content disposition is specified at all. This lets the browser's default handling of the data in the current context to be applied: >>> print http(""" ... GET /abcdefg/@@display HTTP/1.1 ... Authorization: Basic mgr:mgrpw ... """, handle_errors=False) HTTP/1.1 200 Ok Content-Length: 9 Content-Type: text/plain some text zope.file-0.6.2/src/zope/file/tests.py0000664000175000017500000000226011763153771017232 0ustar uliuli00000000000000############################################################################## # # Copyright (c) 2005 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. # ############################################################################## """Unit tests for zope.file. """ __docformat__ = "reStructuredText" import doctest import unittest from zope.file import testing def fromDocFile(path): suite = testing.FunctionalBlobDocFileSuite(path) suite.layer = testing.ZopeFileLayer return suite def test_suite(): return unittest.TestSuite(( doctest.DocFileSuite("README.txt"), doctest.DocFileSuite("browser.txt"), fromDocFile("adapters.txt"), fromDocFile("contenttype.txt"), fromDocFile("download.txt"), fromDocFile("upload.txt"), )) zope.file-0.6.2/src/zope/file/configure.zcml0000664000175000017500000000257311763153771020375 0ustar uliuli00000000000000 zope.file-0.6.2/src/zope/file/testing.py0000664000175000017500000000723411763153771017553 0ustar uliuli00000000000000############################################################################## # # Copyright (c) 2005 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. # ############################################################################## """Functional tests for zope.file. """ __docformat__ = "reStructuredText" import doctest import os.path import shutil import tempfile import transaction from ZODB.DB import DB from ZODB.DemoStorage import DemoStorage from ZODB.blob import BlobStorage import zope.app.testing.functional from zope.app.component.hooks import setSite import ZODB.interfaces here = os.path.dirname(os.path.realpath(__file__)) class FunctionalBlobTestSetup(zope.app.testing.functional.FunctionalTestSetup): temp_dir_name = None direct_blob_support = False def setUp(self): """Prepares for a functional test case.""" # Tear down the old demo storage (if any) and create a fresh one transaction.abort() self.db.close() storage = DemoStorage("Demo Storage", self.base_storage) if ZODB.interfaces.IBlobStorage.providedBy(storage): # at least ZODB 3.9 self.direct_blob_support = True else: # make a dir temp_dir_name = self.temp_dir_name = tempfile.mkdtemp() # wrap storage with BlobStorage storage = BlobStorage(temp_dir_name, storage) self.db = self.app.db = DB(storage) self.connection = None def tearDown(self): """Cleans up after a functional test case.""" transaction.abort() if self.connection: self.connection.close() self.connection = None self.db.close() if not self.direct_blob_support and self.temp_dir_name is not None: # del dir named '__blob_test__%s' % self.name shutil.rmtree(self.temp_dir_name, True) self.temp_dir_name = None setSite(None) class ZCMLLayer(zope.app.testing.functional.ZCMLLayer): def setUp(self): self.setup = FunctionalBlobTestSetup(self.config_file) def FunctionalBlobDocFileSuite(*paths, **kw): globs = kw.setdefault('globs', {}) globs['http'] = zope.app.testing.functional.HTTPCaller() globs['getRootFolder'] = zope.app.testing.functional.getRootFolder globs['sync'] = zope.app.testing.functional.sync kw['package'] = doctest._normalize_module(kw.get('package')) kwsetUp = kw.get('setUp') def setUp(test): FunctionalBlobTestSetup().setUp() if kwsetUp is not None: kwsetUp(test) kw['setUp'] = setUp kwtearDown = kw.get('tearDown') def tearDown(test): if kwtearDown is not None: kwtearDown(test) FunctionalBlobTestSetup().tearDown() kw['tearDown'] = tearDown if 'optionflags' not in kw: old = doctest.set_unittest_reportflags(0) doctest.set_unittest_reportflags(old) kw['optionflags'] = (old | doctest.ELLIPSIS | doctest.REPORT_NDIFF | doctest.NORMALIZE_WHITESPACE) suite = doctest.DocFileSuite(*paths, **kw) suite.layer = zope.app.testing.functional.Functional return suite ZopeFileLayer = ZCMLLayer( os.path.join(here, "ftesting.zcml"), __name__, "ZopeFileLayer") zope.file-0.6.2/src/zope/file/file.py0000664000175000017500000000340411763153771017010 0ustar uliuli00000000000000############################################################################## # # Copyright (c) 2005 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. # ############################################################################## """Implementation of the file content type. """ __docformat__ = "reStructuredText" import persistent import zope.location.interfaces import zope.file.interfaces import zope.interface from ZODB.blob import Blob class File(persistent.Persistent): zope.interface.implements( zope.file.interfaces.IFile, zope.location.interfaces.ILocation) __name__ = None __parent__ = None mimeType = None _data = "" size = 0 def __init__(self, mimeType=None, parameters=None): self.mimeType = mimeType if parameters is None: parameters = {} else: parameters = dict(parameters) self.parameters = parameters self._data = Blob() fp = self._data.open('w') fp.write('') fp.close() def open(self, mode="r"): return self._data.open(mode) def openDetached(self): return file(self._data.committed(), 'rb') @property def size(self): if self._data == "": return 0 reader = self.open() reader.seek(0,2) size = int(reader.tell()) reader.close() return size zope.file-0.6.2/src/zope/file/browser.zcml0000664000175000017500000000172711763153771020077 0ustar uliuli00000000000000 zope.file-0.6.2/src/zope/file/download.py0000664000175000017500000000570411763153771017705 0ustar uliuli00000000000000############################################################################## # # Copyright (c) 2005 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. # ############################################################################## """Download view for files. """ __docformat__ = "reStructuredText" import zope.interface import zope.mimetype.interfaces import zope.publisher.browser import zope.publisher.interfaces.http import zope.security.proxy class Download(zope.publisher.browser.BrowserView): def __call__(self): for k, v in getHeaders(self.context, contentDisposition="attachment"): self.request.response.setHeader(k, v) return DownloadResult(self.context) class Inline(zope.publisher.browser.BrowserView): def __call__(self): for k, v in getHeaders(self.context, contentDisposition="inline"): self.request.response.setHeader(k, v) return DownloadResult(self.context) class Display(zope.publisher.browser.BrowserView): def __call__(self): for k, v in getHeaders(self.context): self.request.response.setHeader(k, v) return DownloadResult(self.context) def getHeaders(context, contentType=None, downloadName=None, contentDisposition=None, contentLength=None): if not contentType: cti = zope.mimetype.interfaces.IContentInfo(context, None) if cti is not None: contentType = cti.contentType contentType = contentType or "application/octet-stream" headers = ("Content-Type", contentType), downloadName = downloadName or context.__name__ if contentDisposition: if downloadName: contentDisposition += ( '; filename="%s"' % downloadName.encode("utf-8") ) headers += ("Content-Disposition", contentDisposition), if contentLength is None: contentLength = context.size headers += ("Content-Length", str(contentLength)), return headers class DownloadResult(object): """Result object for a download request.""" zope.interface.implements(zope.publisher.interfaces.http.IResult) def __init__(self, context): self._iter = bodyIterator( zope.security.proxy.removeSecurityProxy(context.openDetached())) def __iter__(self): return self._iter CHUNK_SIZE = 64 * 1024 def bodyIterator(f): while True: chunk = f.read(CHUNK_SIZE) if not chunk: f.close() raise StopIteration() yield chunk f.close() zope.file-0.6.2/src/zope/file/event.py0000664000175000017500000000225011763153771017210 0ustar uliuli00000000000000############################################################################## # # Copyright (c) 2005 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. # ############################################################################## """Event support code. """ __docformat__ = "reStructuredText" def updateMimeType(file, event): """Update the file's mimeType on a change. If the current mimeType value is known to be legal for the new content type, leave it alone. If not, set the mimeType attribute to the first value from the list of known MIME types for the content type. """ if event.newContentType is not None: types = event.newContentType.getTaggedValue("mimeTypes") if file.mimeType not in types: file.mimeType = types[0] zope.file-0.6.2/src/zope/file/interfaces.py0000664000175000017500000001014511763153771020214 0ustar uliuli00000000000000############################################################################## # # Copyright (c) 2005 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. # ############################################################################## """Interfaces for zope.file. These interfaces support efficient access to file data. """ __docformat__ = "reStructuredText" import zope.interface import zope.mimetype.interfaces import zope.schema from zope.file.i18n import _ class IFile(zope.mimetype.interfaces.IContentTypeAware): """File content object for Zope.""" def open(mode="r"): """Return an object providing access to the file data. Allowed values for `mode` are 'r' (read); 'w' (write); 'a' (append) and 'r+' (read/write). Other values cause `ValueError` to be raised. If the file is opened in read mode, an object with an API (but not necessarily interface) of `IFileReader` is returned; if opened in write mode, an object with an API of `IFileWriter` is returned; if in read/write, an object that implements both is returned. All readers and writers operate in 'binary' mode. """ def openDetached(): """Return file data disconnected from database connection. Read access only. """ size = zope.schema.Int( title=_("Size"), description=_("Size in bytes"), readonly=True, required=True, ) # The remaining interfaces below serve only to document the kind of APIs # to be expected, as described in IFile.open above. class IFileAccessor(zope.interface.Interface): """Base accessor for `IFileReader` and `IFileWriter`.""" def close(): """Release the accessor. Resources held by the accessor are freed, and further use of the accessor will result in a `ValueError` exception. """ mode = zope.schema.BytesLine( title=_("Open mode passed to the corresponding file's open() method," " or the default value if not passed."), required=True, ) class IFileReader(IFileAccessor): """Read-accessor for file objects. The methods here mirror the corresponding portions of the API for Python file objects. """ def read(size=-1): """Read at most `size` bytes from the reader. If the size argument is negative or omitted, read all data until EOF is reached. The bytes are returned as a str. An empty string is returned when EOF is encountered immediately. """ def seek(offset, whence=0): """Set the reader's current position. The `offset` is a number of bytes. The direction and source of the measurement is determined by the `whence` argument. The `whence` argument is optional and defaults to 0 (absolute file positioning); other recognized values are 1 (seek relative to the current position) and 2 (seek relative to the end). There is no return value. """ def tell(): """Return the current position, measured in bytes.""" class IFileWriter(IFileAccessor): """Write-accessor for file objects. The methods here mirror the corresponding portions of the API for Python file objects. """ def flush(): """Ensure the file object has been updated. This is used to make sure that any data buffered in the accessor is stored in the file object, allowing other accessors to use it. """ def write(bytes): """Write a string to the file. Due to buffering, the string may not actually show up in the file until the `flush()` or `close()` method is called. There is no return value. """ zope.file-0.6.2/src/zope/file/ftesting.zcml0000664000175000017500000000246111763153771020233 0ustar uliuli00000000000000 zope.file-0.6.2/src/zope/file/contenttype.txt0000664000175000017500000000657711763153771020652 0ustar uliuli00000000000000================================== Content type and encoding controls ================================== Files provide a view that supports controlling the MIME content type and, where applicable, the content encoding. Content encoding is applicable based on the specific content type of the file. Let's demonstrate the behavior of the form with a simple bit of content. We'll upload a bit of HTML as a sample document: >>> import StringIO >>> sio = StringIO.StringIO("A little HTML." ... " There's one 8-bit Latin-1 character: \xd8.") >>> from zope.testbrowser.testing import Browser >>> browser = Browser() >>> browser.addHeader("Authorization", "Basic mgr:mgrpw") >>> browser.addHeader("Accept-Language", "en-US") >>> browser.open("http://localhost/@@+/zope.file.File") >>> ctrl = browser.getControl(name="form.data") >>> ctrl.mech_control.add_file( ... sio, "text/html", "sample.html") >>> browser.getControl("Add").click() We can see that the MIME handlers have marked this as HTML content: >>> import zope.mimetype.interfaces >>> import zope.mimetype.types >>> file = getRootFolder()["sample.html"] >>> zope.mimetype.types.IContentTypeTextHtml.providedBy(file) True It's important to note that this also means the content is encoded text: >>> zope.mimetype.interfaces.IContentTypeEncoded.providedBy(file) True The "Content Type" page will show us the MIME type and encoding that have been selected: >>> browser.getLink("sample.html").click() >>> browser.getLink("Content Type").click() >>> browser.getControl(name="form.mimeType").value ['zope.mimetype.types.IContentTypeTextHtml'] The empty string value indicates that we have no encoding information: >>> ctrl = browser.getControl(name="form.encoding") >>> print ctrl.value [''] Let's now set the encoding value to an old favorite, Latin-1: >>> ctrl.value = ["iso-8859-1"] >>> browser.handleErrors = False >>> browser.getControl("Save").click() We now see the updated value in the form, and can check the value in the MIME content-type parameters on the object: >>> ctrl = browser.getControl(name="form.encoding") >>> print ctrl.value ['iso-8859-1'] >>> file = getRootFolder()["sample.html"] >>> file.parameters {'charset': 'iso-8859-1'} Something more interesting is that we can now use a non-encoded content type, and the encoding field will be removed from the form: >>> ctrl = browser.getControl(name="form.mimeType") >>> ctrl.value = ["zope.mimetype.types.IContentTypeImageTiff"] >>> browser.getControl("Save").click() >>> browser.getControl(name="form.encoding") Traceback (most recent call last): ... LookupError: name 'form.encoding' If we switch back to an encoded type, we see that our encoding wasn't lost: >>> ctrl = browser.getControl(name="form.mimeType") >>> ctrl.value = ["zope.mimetype.types.IContentTypeTextHtml"] >>> browser.getControl("Save").click() >>> browser.getControl(name="form.encoding").value ['iso-8859-1'] On the other hand, if we try setting the encoding to something which simply cannot decode the input data, we get an error message saying that's not going to work, and no changes are saved: >>> ctrl = browser.getControl(name="form.encoding") >>> ctrl.value = ["utf-8"] >>> browser.getControl("Save").click() >>> print browser.contents <...Selected encoding cannot decode document... zope.file-0.6.2/src/zope/file/contenttype.py0000664000175000017500000001616211763153771020452 0ustar uliuli00000000000000############################################################################## # # Copyright (c) 2005 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. # ############################################################################## """The 'Content Type' view for files. """ __docformat__ = "reStructuredText" import zope.component import zope.formlib.form import zope.formlib.interfaces import zope.interface import zope.lifecycleevent import zope.mimetype.event import zope.mimetype.interfaces import zope.mimetype.source import zope.schema import zope.security.proxy from zope import mimetype from zope.file.i18n import _ def validateCodecUse(file, iface, codec, codec_field): """Validate the use of `codec` for the data in `file`. `iface` is used as the content-type interface for `file`. If `iface` is None, no validation is performed and no error is reported (this validation is considered irrelevant). If `codec` is None, no error is reported, since that indicates that no codec is known to be accurate for the data in `file`. `codec_field` is the form field that is used to select the codec; it is used to generate an error should one be necessary. Returns a list of widget input errors that should be added to any other errors the form determines are relevant. """ errs = [] if (codec is not None) and (iface is not None): if iface.extends(mimetype.interfaces.IContentTypeEncoded): # Need to test that this codec can be used for this # document: f = file.open("r") content_data = f.read() f.close() try: text, consumed = codec.decode(content_data) if consumed != len(content_data): raise UnicodeError("not all data decoded") except UnicodeError: err = zope.formlib.interfaces.WidgetInputError( codec_field.__name__, codec_field.field.title, "Selected encoding cannot decode document.") errs.append(err) return errs class ContentTypeForm(zope.formlib.form.Form): mimeType_field = zope.formlib.form.Field( zope.schema.Choice( __name__="mimeType", title=_("Content type"), description=_("Type of document"), source=mimetype.source.contentTypeSource, )) encoding_field = zope.formlib.form.Field( zope.schema.Choice( __name__="encoding", title=_("Encoding"), description=_("Character data encoding"), source=mimetype.source.codecSource, required=False, )) def get_rendered_encoding(self): charset = self.context.parameters.get("charset") if charset: return zope.component.queryUtility( mimetype.interfaces.ICharsetCodec, charset) else: return None encoding_field.get_rendered = get_rendered_encoding def get_rendered_mimeType(self): # make sure we return the exact `IContentType`-derived # interface that the context provides; there *must* be only # one! ifaces = zope.interface.directlyProvidedBy( zope.security.proxy.removeSecurityProxy(self.context)) for iface in ifaces: if mimetype.interfaces.IContentTypeInterface.providedBy(iface): return iface return None mimeType_field.get_rendered = get_rendered_mimeType def setUpWidgets(self, ignore_request=False): # We need to re-compute the fields before initializing the widgets fields = [self.mimeType_field] if mimetype.interfaces.IContentTypeEncoded.providedBy(self.context): self.have_encoded = True fields.append(self.encoding_field) else: self.have_encoded = False self.form_fields = zope.formlib.form.Fields(*fields) super(ContentTypeForm, self).setUpWidgets( ignore_request=ignore_request) def save_validator(self, action, data): errs = self.validate(None, data) errs += validateCodecUse( self.context, data.get("mimeType"), data.get("encoding"), self.encoding_field) return errs @zope.formlib.form.action(_("Save"), validator=save_validator) def save(self, action, data): context = self.context if data.get("mimeType"): # # XXX Note that the content type parameters are not # modified; ideally, the IContentInfo adapter would filter # the parameters for what makes sense for the current # content type. # iface = data["mimeType"] unwrapped = zope.security.proxy.removeSecurityProxy(context) mimetype.event.changeContentType(unwrapped, iface) # update the encoding, but only if the new content type is encoded encoded = mimetype.interfaces.IContentTypeEncoded.providedBy( context) # We only care about encoding if we're encoded now and were also # encoded before starting the re if encoded and self.have_encoded: codec = data["encoding"] if codec is None: # remove any charset found, since the user said it was wrong; # what it means to have no known charset is that the policy # (IContentInfo) will have to decide how to treat encoding. if "charset" in context.parameters: # We can't just "del" the existing value, since # the security model does not like that. We can # set a new value for context.parameters, though. parameters = dict(context.parameters) del parameters["charset"] context.parameters = parameters else: if "charset" in context.parameters: old_codec = zope.component.queryUtility( mimetype.interfaces.ICharsetCodec, context.parameters["charset"]) else: old_codec = None if getattr(old_codec, "name", None) != codec.name: # use the preferred charset for the new codec new_charset = zope.component.getUtility( mimetype.interfaces.ICodecPreferredCharset, codec.name) parameters = dict(context.parameters) parameters["charset"] = new_charset.name context.parameters = parameters zope.event.notify( zope.lifecycleevent.ObjectModifiedEvent( zope.security.proxy.removeSecurityProxy(context))) zope.file-0.6.2/src/zope/file/adapters.py0000664000175000017500000000263211763153771017676 0ustar uliuli00000000000000############################################################################## # # Copyright (c) 2005 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. # ############################################################################## from zope import interface, component import zope.filerepresentation.interfaces from zope.file import interfaces class ReadFileAdapter(object): component.adapts(interfaces.IFile) interface.implements(zope.filerepresentation.interfaces.IReadFile) def __init__(self, context): self.context = context def size(self): return self.context.size def read(self): f = self.context.open() val = f.read() f.close() return val class WriteFileAdapter(object): component.adapts(interfaces.IFile) interface.implements(zope.filerepresentation.interfaces.IWriteFile) def __init__(self, context): self.context = context def write(self, data): f = self.context.open('w') f.write(data) f.close() zope.file-0.6.2/src/zope/__init__.py0000664000175000017500000000007011763153771016705 0ustar uliuli00000000000000__import__('pkg_resources').declare_namespace(__name__) zope.file-0.6.2/src/zope.file.egg-info/0000775000175000017500000000000011763154606017205 5ustar uliuli00000000000000zope.file-0.6.2/src/zope.file.egg-info/requires.txt0000664000175000017500000000062611763154603021606 0ustar uliuli00000000000000setuptools ZODB3 zope.component zope.browser zope.container zope.contenttype>=3.5 zope.event zope.filerepresentation zope.formlib>=4 zope.i18nmessageid zope.lifecycleevent zope.interface zope.location zope.mimetype zope.publisher zope.schema zope.security zope.size [test] zope.app.component zope.app.server zope.app.testing zope.app.zcmlfiles zope.login zope.password zope.securitypolicy zope.testbrowserzope.file-0.6.2/src/zope.file.egg-info/PKG-INFO0000664000175000017500000007214511763154603020310 0ustar uliuli00000000000000Metadata-Version: 1.0 Name: zope.file Version: 0.6.2 Summary: Efficient File Implementation for Zope Applications Home-page: http://cheeseshop.python.org/pypi/zope.file Author: Zope Corporation and Contributors Author-email: zope-dev@zope.org License: ZPL 2.1 Description: The `zope.file` package provides a content object used to store a file. The interface supports efficient upload and download. .. contents:: =========== File Object =========== The `zope.file` package provides a content object used to store a file. The interface supports efficient upload and download. Let's create an instance: >>> from zope.file.file import File >>> f = File() The object provides a limited number of data attributes. The `mimeType` attribute is used to store the preferred MIME content-type value for the data: >>> f.mimeType >>> f.mimeType = "text/plain" >>> f.mimeType 'text/plain' >>> f.mimeType = "application/postscript" >>> f.mimeType 'application/postscript' The `parameters` attribute is a mapping used to store the content-type parameters. This is where encoding information can be found when applicable (and available): >>> f.parameters {} >>> f.parameters["charset"] = "us-ascii" >>> f.parameters["charset"] 'us-ascii' Both, `parameters` and `mimeType` can optionally also be set when creating a `File` object: >>> f2 = File(mimeType = "application/octet-stream", ... parameters = dict(charset = "utf-8")) >>> f2.mimeType 'application/octet-stream' >>> f2.parameters["charset"] 'utf-8' File objects also sport a `size` attribute that provides the number of bytes in the file: >>> f.size 0 The object supports efficient upload and download by providing all access to content data through accessor objects that provide (subsets of) Python's file API. A file that hasn't been written to is empty. We can get a reader by calling `open()`. Note that all blobs are binary, thus the mode always contains a 'b': >>> r = f.open("r") >>> r.mode 'rb' The `read()` method can be called with a non-negative integer argument to specify how many bytes to read, or with a negative or omitted argument to read to the end of the file: >>> r.read(10) '' >>> r.read() '' >>> r.read(-1) '' Once the accessor has been closed, we can no longer read from it: >>> r.close() >>> r.read() Traceback (most recent call last): ValueError: I/O operation on closed file We'll see that readers are more interesting once there's data in the file object. Data is added by using a writer, which is also created using the `open()` method on the file, but requesting a write file mode: >>> w = f.open("w") >>> w.mode 'wb' The `write()` method is used to add data to the file, but note that the data may be buffered in the writer: >>> w.write("some text ") >>> w.write("more text") The `flush()` method ensure that the data written so far is written to the file object: >>> w.flush() We need to close the file first before determining its file size >>> w.close() >>> f.size 19 We can now use a reader to see that the data has been written to the file: >>> w = f.open("w") >>> w.write('some text more text') >>> w.write(" still more") >>> w.close() >>> f.size 30 Now create a new reader and let's perform some seek operations. >>> r = f.open() The reader also has a `seek()` method that can be used to back up or skip forward in the data stream. Simply passing an offset argument, we see that the current position is moved to that offset from the start of the file: >>> r.seek(20) >>> r.read() 'still more' That's equivalent to passing 0 as the `whence` argument: >>> r.seek(20, 0) >>> r.read() 'still more' We can skip backward and forward relative to the current position by passing 1 for `whence`: >>> r.seek(-10, 1) >>> r.read(5) 'still' >>> r.seek(2, 1) >>> r.read() 'ore' We can skip to some position backward from the end of the file using the value 2 for `whence`: >>> r.seek(-10, 2) >>> r.read() 'still more' >>> r.seek(0) >>> r.seek(-4, 2) >>> r.read() 'more' >>> r.close() Attempting to write to a closed writer raises an exception: >>> w = f.open('w') >>> w.close() >>> w.write('foobar') Traceback (most recent call last): ValueError: I/O operation on closed file Similarly, using `seek()` or `tell()` on a closed reader raises an exception: >>> r.close() >>> r.seek(0) Traceback (most recent call last): ValueError: I/O operation on closed file >>> r.tell() Traceback (most recent call last): ValueError: I/O operation on closed file ======================== Downloading File Objects ======================== The file content type provides a view used to download the file, regardless of the browser's default behavior for the content type. This relies on browser support for the Content-Disposition header. The download support is provided by two distinct objects: A view that provides the download support using the information in the content object, and a result object that can be used to implement a file download by other views. The view can override the content-type or the filename suggested to the browser using the standard IResponse.setHeader method. Note that result objects are intended to be used once and then discarded. Let's start by creating a file object we can use to demonstrate the download support: >>> import transaction >>> from zope.file.file import File >>> f = File() >>> getRootFolder()['file'] = f >>> transaction.commit() Headers ------- Now, let's get the headers for this file. We use a utility function called ``getHeaders``: >>> from zope.file.download import getHeaders >>> headers = getHeaders(f, contentDisposition='attachment') Since there's no suggested download filename on the file, the Content-Disposition header doesn't specify one, but does indicate that the response body be treated as a file to save rather than to apply the default handler for the content type: >>> sorted(headers) [('Content-Disposition', 'attachment; filename="file"'), ('Content-Length', '0'), ('Content-Type', 'application/octet-stream')] Note that a default content type of 'application/octet-stream' is used. If the file object specifies a content type, that's used in the headers by default: >>> f.mimeType = "text/plain" >>> headers = getHeaders(f, contentDisposition='attachment') >>> sorted(headers) [('Content-Disposition', 'attachment; filename="file"'), ('Content-Length', '0'), ('Content-Type', 'text/plain')] Alternatively, a content type can be specified to ``getHeaders``: >>> headers = getHeaders(f, contentType="text/xml", ... contentDisposition='attachment') >>> sorted(headers) [('Content-Disposition', 'attachment; filename="file"'), ('Content-Length', '0'), ('Content-Type', 'text/xml')] The filename provided to the browser can be controlled similarly. If the content object provides one, it will be used by default: >>> headers = getHeaders(f, contentDisposition='attachment') >>> sorted(headers) [('Content-Disposition', 'attachment; filename="file"'), ('Content-Length', '0'), ('Content-Type', 'text/plain')] Providing an alternate name to ``getHeaders`` overrides the download name from the file: >>> headers = getHeaders(f, downloadName="foo.txt", ... contentDisposition='attachment') >>> sorted(headers) [('Content-Disposition', 'attachment; filename="foo.txt"'), ('Content-Length', '0'), ('Content-Type', 'text/plain')] The default Content-Disposition header can be overridden by providing an argument to ``getHeaders``: >>> headers = getHeaders(f, contentDisposition="inline") >>> sorted(headers) [('Content-Disposition', 'inline; filename="file"'), ('Content-Length', '0'), ('Content-Type', 'text/plain')] If the `contentDisposition` argument is not provided, none will be included in the headers: >>> headers = getHeaders(f) >>> sorted(headers) [('Content-Length', '0'), ('Content-Type', 'text/plain')] Body ---- We use DownloadResult to deliver the content to the browser. Since there's no data in this file, there are no body chunks: >>> transaction.commit() >>> from zope.file.download import DownloadResult >>> result = DownloadResult(f) >>> list(result) [] We still need to see how non-empty files are handled. Let's write some data to our file object: >>> w = f.open("w") >>> w.write("some text") >>> w.flush() >>> w.close() >>> transaction.commit() Now we can create a result object and see if we get the data we expect: >>> result = DownloadResult(f) >>> L = list(result) >>> "".join(L) 'some text' If the body content is really large, the iterator may provide more than one chunk of data: >>> w = f.open("w") >>> w.write("*" * 1024 * 1024) >>> w.flush() >>> w.close() >>> transaction.commit() >>> result = DownloadResult(f) >>> L = list(result) >>> len(L) > 1 True Once iteration over the body has completed, further iteration will not yield additional data: >>> list(result) [] The Download View ----------------- Now that we've seen the ``getHeaders`` function and the result object, let's take a look at the basic download view that uses them. We'll need to add a file object where we can get to it using a browser: >>> f = File() >>> f.mimeType = "text/plain" >>> w = f.open("w") >>> w.write("some text") >>> w.close() >>> transaction.commit() >>> getRootFolder()["abcdefg"] = f >>> transaction.commit() Now, let's request the download view of the file object and check the result: >>> print http(""" ... GET /abcdefg/@@download HTTP/1.1 ... Authorization: Basic mgr:mgrpw ... """, handle_errors=False) HTTP/1.1 200 Ok Content-Disposition: attachment; filename="abcdefg" Content-Length: 9 Content-Type: text/plain some text The Inline View --------------- In addition, it is sometimes useful to view the data inline instead of downloading it. A basic inline view is provided for this use case. Note that browsers may decide not to display the image when this view is used and there is not page that it's being loaded into: if this view is being referenced directly via the URL, the browser may show nothing: >>> print http(""" ... GET /abcdefg/@@inline HTTP/1.1 ... Authorization: Basic mgr:mgrpw ... """, handle_errors=False) HTTP/1.1 200 Ok Content-Disposition: inline; filename="abcdefg" Content-Length: 9 Content-Type: text/plain some text The Default Display View ------------------------ This view is similar to the download and inline views, but no content disposition is specified at all. This lets the browser's default handling of the data in the current context to be applied: >>> print http(""" ... GET /abcdefg/@@display HTTP/1.1 ... Authorization: Basic mgr:mgrpw ... """, handle_errors=False) HTTP/1.1 200 Ok Content-Length: 9 Content-Type: text/plain some text ==================== Uploading a new file ==================== There's a simple view for uploading a new file. Let's try it: >>> from StringIO import StringIO >>> sio = StringIO("some text") >>> sio.filename = "plain.txt" >>> sio.headers = {"Content-Type": "text/plain; charset=utf-8", ... "Content-Disposition": 'attachment; filename="plain.txt"'} >>> print http(""" ... POST /@@+/zope.file.File HTTP/1.1 ... Authorization: Basic mgr:mgrpw ... """, form={"form.data": sio, ... "form.actions.add": "Add"}, handle_errors=False) HTTP/1.1 303 ... Now, let's request the download view of the file object and check the result: >>> print http(""" ... GET /plain.txt/@@download HTTP/1.1 ... Authorization: Basic mgr:mgrpw ... """, handle_errors=False) HTTP/1.1 200 Ok Content-Disposition: attachment; filename="plain.txt" Content-Length: 9 Content-Type: text/plain;charset=utf-8 some text We'll peek into the database to make sure the object implements the expected MIME type interface: >>> from zope.mimetype import types >>> ob = getRootFolder()["plain.txt"] >>> types.IContentTypeTextPlain.providedBy(ob) True We can upload new data into our file object as well: >>> sio = StringIO("new text") >>> sio.filename = "stuff.txt" >>> sio.headers = {"Content-Type": "text/plain; charset=utf-8", ... "Content-Disposition": 'attachment; filename="stuff.txt"'} >>> print http(""" ... POST /plain.txt/@@edit.html HTTP/1.1 ... Authorization: Basic mgr:mgrpw ... """, form={"form.data": sio, ... "form.actions.edit": "Edit"}, handle_errors=False) HTTP/1.1 200 ... Now, let's request the download view of the file object and check the result: >>> print http(""" ... GET /plain.txt/@@download HTTP/1.1 ... Authorization: Basic mgr:mgrpw ... """, handle_errors=False) HTTP/1.1 200 Ok Content-Disposition: attachment; filename="plain.txt" Content-Length: 8 Content-Type: text/plain;charset=utf-8 new text If we upload a file that has imprecise content type information (as we expect from browsers generally, and MSIE most significantly), we can see that the MIME type machinery will improve the information where possible: >>> sio = StringIO("\n" ... "...\n") >>> sio.filename = "simple.html" >>> sio.headers = { ... "Content-Type": "text/html; charset=utf-8", ... "Content-Disposition": 'attachment; filename="simple.html"', ... } >>> print http(""" ... POST /@@+/zope.file.File HTTP/1.1 ... Authorization: Basic mgr:mgrpw ... """, form={"form.data": sio, ... "form.actions.add": "Add"}, handle_errors=False) HTTP/1.1 303 ... Again, we'll request the download view of the file object and check the result: >>> print http(""" ... GET /simple.html/@@download HTTP/1.1 ... Authorization: Basic mgr:mgrpw ... """, handle_errors=False) HTTP/1.1 200 Ok Content-Disposition: attachment; filename="simple.html" Content-Length: 56 Content-Type: application/xhtml+xml;charset=utf-8 ... Further, if a browser is bad and sends a full path as the file name (as sometimes happens in many browsers, apparently), the name is correctly truncated and changed. >>> sio = StringIO("\n" ... "...\n") >>> sio.filename = r"C:\Documents and Settings\Joe\naughty name.html" >>> sio.headers = { ... "Content-Type": "text/html; charset=utf-8", ... "Content-Disposition": 'attachment; filename=%s' % sio.filename, ... } >>> print http(""" ... POST /@@+/zope.file.File HTTP/1.1 ... Authorization: Basic mgr:mgrpw ... """, form={"form.data": sio, ... "form.actions.add": "Add"}, handle_errors=False) HTTP/1.1 303 ... Again, we'll request the download view of the file object and check the result: >>> print http(""" ... GET /naughty%20name.html/@@download HTTP/1.1 ... Authorization: Basic mgr:mgrpw ... """, handle_errors=False) HTTP/1.1 200 Ok Content-Disposition: attachment; filename="naughty name.html" Content-Length: 56 Content-Type: application/xhtml+xml;charset=utf-8 ... In zope.file <= 0.5.0, a redundant ObjectCreatedEvent was fired in the Upload view. We'll demonstrate that this is no longer the case. >>> import zope.component >>> from zope.file.interfaces import IFile >>> from zope.lifecycleevent import IObjectCreatedEvent We'll register a subscriber for IObjectCreatedEvent that simply increments a counter. >>> count = 0 >>> def inc(*args): ... global count; count += 1 >>> zope.component.provideHandler(inc, (IFile, IObjectCreatedEvent)) >>> print http(""" ... POST /@@+/zope.file.File HTTP/1.1 ... Authorization: Basic mgr:mgrpw ... """, form={"form.data": sio, ... "form.actions.add": "Add"}, handle_errors=False) HTTP/1.1 303 ... The subscriber was called only once. >>> print count 1 ================================== Content type and encoding controls ================================== Files provide a view that supports controlling the MIME content type and, where applicable, the content encoding. Content encoding is applicable based on the specific content type of the file. Let's demonstrate the behavior of the form with a simple bit of content. We'll upload a bit of HTML as a sample document: >>> import StringIO >>> sio = StringIO.StringIO("A little HTML." ... " There's one 8-bit Latin-1 character: \xd8.") >>> from zope.testbrowser.testing import Browser >>> browser = Browser() >>> browser.addHeader("Authorization", "Basic mgr:mgrpw") >>> browser.addHeader("Accept-Language", "en-US") >>> browser.open("http://localhost/@@+/zope.file.File") >>> ctrl = browser.getControl(name="form.data") >>> ctrl.mech_control.add_file( ... sio, "text/html", "sample.html") >>> browser.getControl("Add").click() We can see that the MIME handlers have marked this as HTML content: >>> import zope.mimetype.interfaces >>> import zope.mimetype.types >>> file = getRootFolder()["sample.html"] >>> zope.mimetype.types.IContentTypeTextHtml.providedBy(file) True It's important to note that this also means the content is encoded text: >>> zope.mimetype.interfaces.IContentTypeEncoded.providedBy(file) True The "Content Type" page will show us the MIME type and encoding that have been selected: >>> browser.getLink("sample.html").click() >>> browser.getLink("Content Type").click() >>> browser.getControl(name="form.mimeType").value ['zope.mimetype.types.IContentTypeTextHtml'] The empty string value indicates that we have no encoding information: >>> ctrl = browser.getControl(name="form.encoding") >>> print ctrl.value [''] Let's now set the encoding value to an old favorite, Latin-1: >>> ctrl.value = ["iso-8859-1"] >>> browser.handleErrors = False >>> browser.getControl("Save").click() We now see the updated value in the form, and can check the value in the MIME content-type parameters on the object: >>> ctrl = browser.getControl(name="form.encoding") >>> print ctrl.value ['iso-8859-1'] >>> file = getRootFolder()["sample.html"] >>> file.parameters {'charset': 'iso-8859-1'} Something more interesting is that we can now use a non-encoded content type, and the encoding field will be removed from the form: >>> ctrl = browser.getControl(name="form.mimeType") >>> ctrl.value = ["zope.mimetype.types.IContentTypeImageTiff"] >>> browser.getControl("Save").click() >>> browser.getControl(name="form.encoding") Traceback (most recent call last): ... LookupError: name 'form.encoding' If we switch back to an encoded type, we see that our encoding wasn't lost: >>> ctrl = browser.getControl(name="form.mimeType") >>> ctrl.value = ["zope.mimetype.types.IContentTypeTextHtml"] >>> browser.getControl("Save").click() >>> browser.getControl(name="form.encoding").value ['iso-8859-1'] On the other hand, if we try setting the encoding to something which simply cannot decode the input data, we get an error message saying that's not going to work, and no changes are saved: >>> ctrl = browser.getControl(name="form.encoding") >>> ctrl.value = ["utf-8"] >>> browser.getControl("Save").click() >>> print browser.contents <...Selected encoding cannot decode document... ===================== Presentation Adapters ===================== Object size ----------- The size of the file as presented in the contents view of a container is provided using an adapter implementing the `zope.size.interfaces.ISized` interface. Such an adapter is available for the file object. Let's do some imports and create a new file object: >>> from zope.file.file import File >>> from zope.file.browser import Sized >>> from zope.size.interfaces import ISized >>> f = File() >>> f.size 0 >>> s = Sized(f) >>> ISized.providedBy(s) True >>> s.sizeForSorting() ('byte', 0) >>> s.sizeForDisplay() u'0 KB' Let's add some content to the file: >>> w = f.open('w') >>> w.write("some text") >>> w.close() The sized adapter now reflects the updated size: >>> s.sizeForSorting() ('byte', 9) >>> s.sizeForDisplay() u'1 KB' Let's try again with a larger file size: >>> w = f.open('w') >>> w.write("x" * (1024*1024+10)) >>> w.close() >>> s.sizeForSorting() ('byte', 1048586) >>> m = s.sizeForDisplay() >>> m u'${size} MB' >>> m.mapping {'size': '1.00'} And still a bigger size: >>> w = f.open('w') >>> w.write("x" * 3*512*1024) >>> w.close() >>> s.sizeForSorting() ('byte', 1572864) >>> m = s.sizeForDisplay() >>> m u'${size} MB' >>> m.mapping {'size': '1.50'} ======= CHANGES ======= 0.6.2 (2012-06-04) ------------------ - Moved menu-oriented registrations into new menus.zcml. This is now loaded if zope.app.zcmlfiles is available only. - Increase test coverage. 0.6.1 (2012-01-26) ------------------ - Declared more dependencies. 0.6.0 (2010-09-16) ------------------ - Bug fix: remove duplicate firing of ObjectCreatedEvent in zope.file.upload.Upload (the event is already fired in its base class, zope.formlib.form.AddForm). - Move browser-related zcml to `browser.zcml` so that it easier for applications to exclude it. - Import content-type parser from zope.contenttype, adding a dependency on that package. - Removed undeclared dependency on zope.app.container, depend on zope.browser. - Using Python's ``doctest`` module instead of deprecated ``zope.testing.doctest``. 0.5.0 (2009-07-23) ------------------ - Change package's mailing list address to zope-dev at zope.org instead of the retired one. - Made tests compatible with ZODB 3.9. - Removed not needed install requirement declarations. 0.4.0 (2009-01-31) ------------------ - `openDetached` is now protected by zope.View instead of zope.ManageContent. - Use zope.container instead of zope.app.container. 0.3.0 (2007-11-01) ------------------ - Package data update. 0.2.0 (2007-04-18) ------------------ - Fix code for Publisher version 3.4. 0.1.0 (2007-04-18) ------------------ - Initial release. Keywords: zope3 web html ui file pattern Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Zope Public License Classifier: Programming Language :: Python Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Topic :: Internet :: WWW/HTTP Classifier: Framework :: Zope3 zope.file-0.6.2/src/zope.file.egg-info/dependency_links.txt0000664000175000017500000000000111763154603023250 0ustar uliuli00000000000000 zope.file-0.6.2/src/zope.file.egg-info/SOURCES.txt0000664000175000017500000000164611763154603021075 0ustar uliuli00000000000000CHANGES.txt README.txt ZopePublicLicense.txt bootstrap.py buildout.cfg setup.py src/zope/__init__.py src/zope.file.egg-info/PKG-INFO src/zope.file.egg-info/SOURCES.txt src/zope.file.egg-info/dependency_links.txt src/zope.file.egg-info/namespace_packages.txt src/zope.file.egg-info/not-zip-safe src/zope.file.egg-info/requires.txt src/zope.file.egg-info/top_level.txt src/zope/file/README.txt src/zope/file/__init__.py src/zope/file/adapters.py src/zope/file/adapters.txt src/zope/file/browser.py src/zope/file/browser.txt src/zope/file/browser.zcml src/zope/file/configure.zcml src/zope/file/contenttype.py src/zope/file/contenttype.txt src/zope/file/download.py src/zope/file/download.txt src/zope/file/event.py src/zope/file/file.py src/zope/file/ftesting.zcml src/zope/file/i18n.py src/zope/file/interfaces.py src/zope/file/menus.zcml src/zope/file/testing.py src/zope/file/tests.py src/zope/file/upload.py src/zope/file/upload.txtzope.file-0.6.2/src/zope.file.egg-info/top_level.txt0000664000175000017500000000000511763154603021727 0ustar uliuli00000000000000zope zope.file-0.6.2/src/zope.file.egg-info/not-zip-safe0000664000175000017500000000000111763154052021426 0ustar uliuli00000000000000 zope.file-0.6.2/src/zope.file.egg-info/namespace_packages.txt0000664000175000017500000000000511763154603023530 0ustar uliuli00000000000000zope zope.file-0.6.2/PKG-INFO0000664000175000017500000007214511763154606014137 0ustar uliuli00000000000000Metadata-Version: 1.0 Name: zope.file Version: 0.6.2 Summary: Efficient File Implementation for Zope Applications Home-page: http://cheeseshop.python.org/pypi/zope.file Author: Zope Corporation and Contributors Author-email: zope-dev@zope.org License: ZPL 2.1 Description: The `zope.file` package provides a content object used to store a file. The interface supports efficient upload and download. .. contents:: =========== File Object =========== The `zope.file` package provides a content object used to store a file. The interface supports efficient upload and download. Let's create an instance: >>> from zope.file.file import File >>> f = File() The object provides a limited number of data attributes. The `mimeType` attribute is used to store the preferred MIME content-type value for the data: >>> f.mimeType >>> f.mimeType = "text/plain" >>> f.mimeType 'text/plain' >>> f.mimeType = "application/postscript" >>> f.mimeType 'application/postscript' The `parameters` attribute is a mapping used to store the content-type parameters. This is where encoding information can be found when applicable (and available): >>> f.parameters {} >>> f.parameters["charset"] = "us-ascii" >>> f.parameters["charset"] 'us-ascii' Both, `parameters` and `mimeType` can optionally also be set when creating a `File` object: >>> f2 = File(mimeType = "application/octet-stream", ... parameters = dict(charset = "utf-8")) >>> f2.mimeType 'application/octet-stream' >>> f2.parameters["charset"] 'utf-8' File objects also sport a `size` attribute that provides the number of bytes in the file: >>> f.size 0 The object supports efficient upload and download by providing all access to content data through accessor objects that provide (subsets of) Python's file API. A file that hasn't been written to is empty. We can get a reader by calling `open()`. Note that all blobs are binary, thus the mode always contains a 'b': >>> r = f.open("r") >>> r.mode 'rb' The `read()` method can be called with a non-negative integer argument to specify how many bytes to read, or with a negative or omitted argument to read to the end of the file: >>> r.read(10) '' >>> r.read() '' >>> r.read(-1) '' Once the accessor has been closed, we can no longer read from it: >>> r.close() >>> r.read() Traceback (most recent call last): ValueError: I/O operation on closed file We'll see that readers are more interesting once there's data in the file object. Data is added by using a writer, which is also created using the `open()` method on the file, but requesting a write file mode: >>> w = f.open("w") >>> w.mode 'wb' The `write()` method is used to add data to the file, but note that the data may be buffered in the writer: >>> w.write("some text ") >>> w.write("more text") The `flush()` method ensure that the data written so far is written to the file object: >>> w.flush() We need to close the file first before determining its file size >>> w.close() >>> f.size 19 We can now use a reader to see that the data has been written to the file: >>> w = f.open("w") >>> w.write('some text more text') >>> w.write(" still more") >>> w.close() >>> f.size 30 Now create a new reader and let's perform some seek operations. >>> r = f.open() The reader also has a `seek()` method that can be used to back up or skip forward in the data stream. Simply passing an offset argument, we see that the current position is moved to that offset from the start of the file: >>> r.seek(20) >>> r.read() 'still more' That's equivalent to passing 0 as the `whence` argument: >>> r.seek(20, 0) >>> r.read() 'still more' We can skip backward and forward relative to the current position by passing 1 for `whence`: >>> r.seek(-10, 1) >>> r.read(5) 'still' >>> r.seek(2, 1) >>> r.read() 'ore' We can skip to some position backward from the end of the file using the value 2 for `whence`: >>> r.seek(-10, 2) >>> r.read() 'still more' >>> r.seek(0) >>> r.seek(-4, 2) >>> r.read() 'more' >>> r.close() Attempting to write to a closed writer raises an exception: >>> w = f.open('w') >>> w.close() >>> w.write('foobar') Traceback (most recent call last): ValueError: I/O operation on closed file Similarly, using `seek()` or `tell()` on a closed reader raises an exception: >>> r.close() >>> r.seek(0) Traceback (most recent call last): ValueError: I/O operation on closed file >>> r.tell() Traceback (most recent call last): ValueError: I/O operation on closed file ======================== Downloading File Objects ======================== The file content type provides a view used to download the file, regardless of the browser's default behavior for the content type. This relies on browser support for the Content-Disposition header. The download support is provided by two distinct objects: A view that provides the download support using the information in the content object, and a result object that can be used to implement a file download by other views. The view can override the content-type or the filename suggested to the browser using the standard IResponse.setHeader method. Note that result objects are intended to be used once and then discarded. Let's start by creating a file object we can use to demonstrate the download support: >>> import transaction >>> from zope.file.file import File >>> f = File() >>> getRootFolder()['file'] = f >>> transaction.commit() Headers ------- Now, let's get the headers for this file. We use a utility function called ``getHeaders``: >>> from zope.file.download import getHeaders >>> headers = getHeaders(f, contentDisposition='attachment') Since there's no suggested download filename on the file, the Content-Disposition header doesn't specify one, but does indicate that the response body be treated as a file to save rather than to apply the default handler for the content type: >>> sorted(headers) [('Content-Disposition', 'attachment; filename="file"'), ('Content-Length', '0'), ('Content-Type', 'application/octet-stream')] Note that a default content type of 'application/octet-stream' is used. If the file object specifies a content type, that's used in the headers by default: >>> f.mimeType = "text/plain" >>> headers = getHeaders(f, contentDisposition='attachment') >>> sorted(headers) [('Content-Disposition', 'attachment; filename="file"'), ('Content-Length', '0'), ('Content-Type', 'text/plain')] Alternatively, a content type can be specified to ``getHeaders``: >>> headers = getHeaders(f, contentType="text/xml", ... contentDisposition='attachment') >>> sorted(headers) [('Content-Disposition', 'attachment; filename="file"'), ('Content-Length', '0'), ('Content-Type', 'text/xml')] The filename provided to the browser can be controlled similarly. If the content object provides one, it will be used by default: >>> headers = getHeaders(f, contentDisposition='attachment') >>> sorted(headers) [('Content-Disposition', 'attachment; filename="file"'), ('Content-Length', '0'), ('Content-Type', 'text/plain')] Providing an alternate name to ``getHeaders`` overrides the download name from the file: >>> headers = getHeaders(f, downloadName="foo.txt", ... contentDisposition='attachment') >>> sorted(headers) [('Content-Disposition', 'attachment; filename="foo.txt"'), ('Content-Length', '0'), ('Content-Type', 'text/plain')] The default Content-Disposition header can be overridden by providing an argument to ``getHeaders``: >>> headers = getHeaders(f, contentDisposition="inline") >>> sorted(headers) [('Content-Disposition', 'inline; filename="file"'), ('Content-Length', '0'), ('Content-Type', 'text/plain')] If the `contentDisposition` argument is not provided, none will be included in the headers: >>> headers = getHeaders(f) >>> sorted(headers) [('Content-Length', '0'), ('Content-Type', 'text/plain')] Body ---- We use DownloadResult to deliver the content to the browser. Since there's no data in this file, there are no body chunks: >>> transaction.commit() >>> from zope.file.download import DownloadResult >>> result = DownloadResult(f) >>> list(result) [] We still need to see how non-empty files are handled. Let's write some data to our file object: >>> w = f.open("w") >>> w.write("some text") >>> w.flush() >>> w.close() >>> transaction.commit() Now we can create a result object and see if we get the data we expect: >>> result = DownloadResult(f) >>> L = list(result) >>> "".join(L) 'some text' If the body content is really large, the iterator may provide more than one chunk of data: >>> w = f.open("w") >>> w.write("*" * 1024 * 1024) >>> w.flush() >>> w.close() >>> transaction.commit() >>> result = DownloadResult(f) >>> L = list(result) >>> len(L) > 1 True Once iteration over the body has completed, further iteration will not yield additional data: >>> list(result) [] The Download View ----------------- Now that we've seen the ``getHeaders`` function and the result object, let's take a look at the basic download view that uses them. We'll need to add a file object where we can get to it using a browser: >>> f = File() >>> f.mimeType = "text/plain" >>> w = f.open("w") >>> w.write("some text") >>> w.close() >>> transaction.commit() >>> getRootFolder()["abcdefg"] = f >>> transaction.commit() Now, let's request the download view of the file object and check the result: >>> print http(""" ... GET /abcdefg/@@download HTTP/1.1 ... Authorization: Basic mgr:mgrpw ... """, handle_errors=False) HTTP/1.1 200 Ok Content-Disposition: attachment; filename="abcdefg" Content-Length: 9 Content-Type: text/plain some text The Inline View --------------- In addition, it is sometimes useful to view the data inline instead of downloading it. A basic inline view is provided for this use case. Note that browsers may decide not to display the image when this view is used and there is not page that it's being loaded into: if this view is being referenced directly via the URL, the browser may show nothing: >>> print http(""" ... GET /abcdefg/@@inline HTTP/1.1 ... Authorization: Basic mgr:mgrpw ... """, handle_errors=False) HTTP/1.1 200 Ok Content-Disposition: inline; filename="abcdefg" Content-Length: 9 Content-Type: text/plain some text The Default Display View ------------------------ This view is similar to the download and inline views, but no content disposition is specified at all. This lets the browser's default handling of the data in the current context to be applied: >>> print http(""" ... GET /abcdefg/@@display HTTP/1.1 ... Authorization: Basic mgr:mgrpw ... """, handle_errors=False) HTTP/1.1 200 Ok Content-Length: 9 Content-Type: text/plain some text ==================== Uploading a new file ==================== There's a simple view for uploading a new file. Let's try it: >>> from StringIO import StringIO >>> sio = StringIO("some text") >>> sio.filename = "plain.txt" >>> sio.headers = {"Content-Type": "text/plain; charset=utf-8", ... "Content-Disposition": 'attachment; filename="plain.txt"'} >>> print http(""" ... POST /@@+/zope.file.File HTTP/1.1 ... Authorization: Basic mgr:mgrpw ... """, form={"form.data": sio, ... "form.actions.add": "Add"}, handle_errors=False) HTTP/1.1 303 ... Now, let's request the download view of the file object and check the result: >>> print http(""" ... GET /plain.txt/@@download HTTP/1.1 ... Authorization: Basic mgr:mgrpw ... """, handle_errors=False) HTTP/1.1 200 Ok Content-Disposition: attachment; filename="plain.txt" Content-Length: 9 Content-Type: text/plain;charset=utf-8 some text We'll peek into the database to make sure the object implements the expected MIME type interface: >>> from zope.mimetype import types >>> ob = getRootFolder()["plain.txt"] >>> types.IContentTypeTextPlain.providedBy(ob) True We can upload new data into our file object as well: >>> sio = StringIO("new text") >>> sio.filename = "stuff.txt" >>> sio.headers = {"Content-Type": "text/plain; charset=utf-8", ... "Content-Disposition": 'attachment; filename="stuff.txt"'} >>> print http(""" ... POST /plain.txt/@@edit.html HTTP/1.1 ... Authorization: Basic mgr:mgrpw ... """, form={"form.data": sio, ... "form.actions.edit": "Edit"}, handle_errors=False) HTTP/1.1 200 ... Now, let's request the download view of the file object and check the result: >>> print http(""" ... GET /plain.txt/@@download HTTP/1.1 ... Authorization: Basic mgr:mgrpw ... """, handle_errors=False) HTTP/1.1 200 Ok Content-Disposition: attachment; filename="plain.txt" Content-Length: 8 Content-Type: text/plain;charset=utf-8 new text If we upload a file that has imprecise content type information (as we expect from browsers generally, and MSIE most significantly), we can see that the MIME type machinery will improve the information where possible: >>> sio = StringIO("\n" ... "...\n") >>> sio.filename = "simple.html" >>> sio.headers = { ... "Content-Type": "text/html; charset=utf-8", ... "Content-Disposition": 'attachment; filename="simple.html"', ... } >>> print http(""" ... POST /@@+/zope.file.File HTTP/1.1 ... Authorization: Basic mgr:mgrpw ... """, form={"form.data": sio, ... "form.actions.add": "Add"}, handle_errors=False) HTTP/1.1 303 ... Again, we'll request the download view of the file object and check the result: >>> print http(""" ... GET /simple.html/@@download HTTP/1.1 ... Authorization: Basic mgr:mgrpw ... """, handle_errors=False) HTTP/1.1 200 Ok Content-Disposition: attachment; filename="simple.html" Content-Length: 56 Content-Type: application/xhtml+xml;charset=utf-8 ... Further, if a browser is bad and sends a full path as the file name (as sometimes happens in many browsers, apparently), the name is correctly truncated and changed. >>> sio = StringIO("\n" ... "...\n") >>> sio.filename = r"C:\Documents and Settings\Joe\naughty name.html" >>> sio.headers = { ... "Content-Type": "text/html; charset=utf-8", ... "Content-Disposition": 'attachment; filename=%s' % sio.filename, ... } >>> print http(""" ... POST /@@+/zope.file.File HTTP/1.1 ... Authorization: Basic mgr:mgrpw ... """, form={"form.data": sio, ... "form.actions.add": "Add"}, handle_errors=False) HTTP/1.1 303 ... Again, we'll request the download view of the file object and check the result: >>> print http(""" ... GET /naughty%20name.html/@@download HTTP/1.1 ... Authorization: Basic mgr:mgrpw ... """, handle_errors=False) HTTP/1.1 200 Ok Content-Disposition: attachment; filename="naughty name.html" Content-Length: 56 Content-Type: application/xhtml+xml;charset=utf-8 ... In zope.file <= 0.5.0, a redundant ObjectCreatedEvent was fired in the Upload view. We'll demonstrate that this is no longer the case. >>> import zope.component >>> from zope.file.interfaces import IFile >>> from zope.lifecycleevent import IObjectCreatedEvent We'll register a subscriber for IObjectCreatedEvent that simply increments a counter. >>> count = 0 >>> def inc(*args): ... global count; count += 1 >>> zope.component.provideHandler(inc, (IFile, IObjectCreatedEvent)) >>> print http(""" ... POST /@@+/zope.file.File HTTP/1.1 ... Authorization: Basic mgr:mgrpw ... """, form={"form.data": sio, ... "form.actions.add": "Add"}, handle_errors=False) HTTP/1.1 303 ... The subscriber was called only once. >>> print count 1 ================================== Content type and encoding controls ================================== Files provide a view that supports controlling the MIME content type and, where applicable, the content encoding. Content encoding is applicable based on the specific content type of the file. Let's demonstrate the behavior of the form with a simple bit of content. We'll upload a bit of HTML as a sample document: >>> import StringIO >>> sio = StringIO.StringIO("A little HTML." ... " There's one 8-bit Latin-1 character: \xd8.") >>> from zope.testbrowser.testing import Browser >>> browser = Browser() >>> browser.addHeader("Authorization", "Basic mgr:mgrpw") >>> browser.addHeader("Accept-Language", "en-US") >>> browser.open("http://localhost/@@+/zope.file.File") >>> ctrl = browser.getControl(name="form.data") >>> ctrl.mech_control.add_file( ... sio, "text/html", "sample.html") >>> browser.getControl("Add").click() We can see that the MIME handlers have marked this as HTML content: >>> import zope.mimetype.interfaces >>> import zope.mimetype.types >>> file = getRootFolder()["sample.html"] >>> zope.mimetype.types.IContentTypeTextHtml.providedBy(file) True It's important to note that this also means the content is encoded text: >>> zope.mimetype.interfaces.IContentTypeEncoded.providedBy(file) True The "Content Type" page will show us the MIME type and encoding that have been selected: >>> browser.getLink("sample.html").click() >>> browser.getLink("Content Type").click() >>> browser.getControl(name="form.mimeType").value ['zope.mimetype.types.IContentTypeTextHtml'] The empty string value indicates that we have no encoding information: >>> ctrl = browser.getControl(name="form.encoding") >>> print ctrl.value [''] Let's now set the encoding value to an old favorite, Latin-1: >>> ctrl.value = ["iso-8859-1"] >>> browser.handleErrors = False >>> browser.getControl("Save").click() We now see the updated value in the form, and can check the value in the MIME content-type parameters on the object: >>> ctrl = browser.getControl(name="form.encoding") >>> print ctrl.value ['iso-8859-1'] >>> file = getRootFolder()["sample.html"] >>> file.parameters {'charset': 'iso-8859-1'} Something more interesting is that we can now use a non-encoded content type, and the encoding field will be removed from the form: >>> ctrl = browser.getControl(name="form.mimeType") >>> ctrl.value = ["zope.mimetype.types.IContentTypeImageTiff"] >>> browser.getControl("Save").click() >>> browser.getControl(name="form.encoding") Traceback (most recent call last): ... LookupError: name 'form.encoding' If we switch back to an encoded type, we see that our encoding wasn't lost: >>> ctrl = browser.getControl(name="form.mimeType") >>> ctrl.value = ["zope.mimetype.types.IContentTypeTextHtml"] >>> browser.getControl("Save").click() >>> browser.getControl(name="form.encoding").value ['iso-8859-1'] On the other hand, if we try setting the encoding to something which simply cannot decode the input data, we get an error message saying that's not going to work, and no changes are saved: >>> ctrl = browser.getControl(name="form.encoding") >>> ctrl.value = ["utf-8"] >>> browser.getControl("Save").click() >>> print browser.contents <...Selected encoding cannot decode document... ===================== Presentation Adapters ===================== Object size ----------- The size of the file as presented in the contents view of a container is provided using an adapter implementing the `zope.size.interfaces.ISized` interface. Such an adapter is available for the file object. Let's do some imports and create a new file object: >>> from zope.file.file import File >>> from zope.file.browser import Sized >>> from zope.size.interfaces import ISized >>> f = File() >>> f.size 0 >>> s = Sized(f) >>> ISized.providedBy(s) True >>> s.sizeForSorting() ('byte', 0) >>> s.sizeForDisplay() u'0 KB' Let's add some content to the file: >>> w = f.open('w') >>> w.write("some text") >>> w.close() The sized adapter now reflects the updated size: >>> s.sizeForSorting() ('byte', 9) >>> s.sizeForDisplay() u'1 KB' Let's try again with a larger file size: >>> w = f.open('w') >>> w.write("x" * (1024*1024+10)) >>> w.close() >>> s.sizeForSorting() ('byte', 1048586) >>> m = s.sizeForDisplay() >>> m u'${size} MB' >>> m.mapping {'size': '1.00'} And still a bigger size: >>> w = f.open('w') >>> w.write("x" * 3*512*1024) >>> w.close() >>> s.sizeForSorting() ('byte', 1572864) >>> m = s.sizeForDisplay() >>> m u'${size} MB' >>> m.mapping {'size': '1.50'} ======= CHANGES ======= 0.6.2 (2012-06-04) ------------------ - Moved menu-oriented registrations into new menus.zcml. This is now loaded if zope.app.zcmlfiles is available only. - Increase test coverage. 0.6.1 (2012-01-26) ------------------ - Declared more dependencies. 0.6.0 (2010-09-16) ------------------ - Bug fix: remove duplicate firing of ObjectCreatedEvent in zope.file.upload.Upload (the event is already fired in its base class, zope.formlib.form.AddForm). - Move browser-related zcml to `browser.zcml` so that it easier for applications to exclude it. - Import content-type parser from zope.contenttype, adding a dependency on that package. - Removed undeclared dependency on zope.app.container, depend on zope.browser. - Using Python's ``doctest`` module instead of deprecated ``zope.testing.doctest``. 0.5.0 (2009-07-23) ------------------ - Change package's mailing list address to zope-dev at zope.org instead of the retired one. - Made tests compatible with ZODB 3.9. - Removed not needed install requirement declarations. 0.4.0 (2009-01-31) ------------------ - `openDetached` is now protected by zope.View instead of zope.ManageContent. - Use zope.container instead of zope.app.container. 0.3.0 (2007-11-01) ------------------ - Package data update. 0.2.0 (2007-04-18) ------------------ - Fix code for Publisher version 3.4. 0.1.0 (2007-04-18) ------------------ - Initial release. Keywords: zope3 web html ui file pattern Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Zope Public License Classifier: Programming Language :: Python Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Topic :: Internet :: WWW/HTTP Classifier: Framework :: Zope3 zope.file-0.6.2/setup.py0000664000175000017500000000656411763154432014553 0ustar uliuli00000000000000############################################################################## # # Copyright (c) 2006-2009 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. # ############################################################################## """Setup for zope.file package $Id: setup.py 80818 2007-10-11 04:06:12Z srichter $ """ import os from setuptools import setup, find_packages def read(*rnames): return open(os.path.join(os.path.dirname(__file__), *rnames)).read() setup(name='zope.file', version='0.6.2', author='Zope Corporation and Contributors', author_email='zope-dev@zope.org', description='Efficient File Implementation for Zope Applications', long_description=( read('README.txt') + '\n\n' + '.. contents::' + '\n\n' + read('src', 'zope', 'file', 'README.txt') + '\n\n' + read('src', 'zope', 'file', 'download.txt') + '\n\n' + read('src', 'zope', 'file', 'upload.txt') + '\n\n' + read('src', 'zope', 'file', 'contenttype.txt') + '\n\n' + read('src', 'zope', 'file', 'browser.txt') + '\n\n' + read('CHANGES.txt') ), keywords = "zope3 web html ui file pattern", classifiers = [ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', 'Intended Audience :: Developers', 'License :: OSI Approved :: Zope Public License', 'Programming Language :: Python', 'Natural Language :: English', 'Operating System :: OS Independent', 'Topic :: Internet :: WWW/HTTP', 'Framework :: Zope3'], url='http://cheeseshop.python.org/pypi/zope.file', license='ZPL 2.1', packages=find_packages('src'), package_dir = {'': 'src'}, namespace_packages=['zope'], extras_require = dict( test=['zope.app.component', 'zope.app.server', 'zope.app.testing', 'zope.app.zcmlfiles', 'zope.login', 'zope.password', 'zope.securitypolicy', 'zope.testbrowser']), install_requires=['setuptools', 'ZODB3', 'zope.component', 'zope.browser', 'zope.container', 'zope.contenttype>=3.5', 'zope.event', 'zope.filerepresentation', 'zope.formlib>=4', 'zope.i18nmessageid', 'zope.lifecycleevent', 'zope.interface', 'zope.location', 'zope.mimetype', 'zope.publisher', 'zope.schema', 'zope.security', 'zope.size', ], include_package_data = True, zip_safe = False, ) zope.file-0.6.2/README.txt0000664000175000017500000000017611763153771014535 0ustar uliuli00000000000000The `zope.file` package provides a content object used to store a file. The interface supports efficient upload and download. zope.file-0.6.2/ZopePublicLicense.txt0000664000175000017500000000420311763153771017152 0ustar uliuli00000000000000Zope 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. zope.file-0.6.2/buildout.cfg0000664000175000017500000000064611763153771015351 0ustar uliuli00000000000000[buildout] develop = . parts = test coverage-test coverage-report [test] recipe = zc.recipe.testrunner eggs = zope.file [test] [coverage-test] recipe = zc.recipe.testrunner eggs = zope.file [test] defaults = ['--tests-pattern', '^f?tests$', '-v', '--coverage', '../../coverage'] [coverage-report] recipe = zc.recipe.egg eggs = z3c.coverage scripts = coverage=coverage-report arguments = ('coverage', 'coverage/report') zope.file-0.6.2/bootstrap.py0000664000175000017500000000336711763153771015433 0ustar uliuli00000000000000############################################################################## # # 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 69908 2006-08-31 21:53:00Z jim $ """ import os, shutil, sys, tempfile, urllib2 tmpeggs = tempfile.mkdtemp() 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) zope.file-0.6.2/CHANGES.txt0000664000175000017500000000264411763153771014652 0ustar uliuli00000000000000======= CHANGES ======= 0.6.2 (2012-06-04) ------------------ - Moved menu-oriented registrations into new menus.zcml. This is now loaded if zope.app.zcmlfiles is available only. - Increase test coverage. 0.6.1 (2012-01-26) ------------------ - Declared more dependencies. 0.6.0 (2010-09-16) ------------------ - Bug fix: remove duplicate firing of ObjectCreatedEvent in zope.file.upload.Upload (the event is already fired in its base class, zope.formlib.form.AddForm). - Move browser-related zcml to `browser.zcml` so that it easier for applications to exclude it. - Import content-type parser from zope.contenttype, adding a dependency on that package. - Removed undeclared dependency on zope.app.container, depend on zope.browser. - Using Python's ``doctest`` module instead of deprecated ``zope.testing.doctest``. 0.5.0 (2009-07-23) ------------------ - Change package's mailing list address to zope-dev at zope.org instead of the retired one. - Made tests compatible with ZODB 3.9. - Removed not needed install requirement declarations. 0.4.0 (2009-01-31) ------------------ - `openDetached` is now protected by zope.View instead of zope.ManageContent. - Use zope.container instead of zope.app.container. 0.3.0 (2007-11-01) ------------------ - Package data update. 0.2.0 (2007-04-18) ------------------ - Fix code for Publisher version 3.4. 0.1.0 (2007-04-18) ------------------ - Initial release.