= Serving directories of files = LAZR adds special views that can be used to serve all the files under a particular directory. == ExportedFolder == This is the base implementation. To export a directory, you need to subclass that view and provide a folder property returning the path of the directory to expose. >>> import os >>> import tempfile >>> resource_dir = tempfile.mkdtemp(prefix='resources') >>> file(os.path.join(resource_dir, 'test.txt'), 'w').write('Text file') >>> file(os.path.join(resource_dir, 'image1.gif'), 'w').write( ... 'GIF file') >>> file(os.path.join(resource_dir, 'image2.png'), 'w').write( ... 'PNG file') >>> os.mkdir(os.path.join(resource_dir, 'a_dir')) >>> file(os.path.join(resource_dir, 'other.txt'), 'w').write( ... 'Other file') >>> from lp.app.browser.folder import ExportedFolder >>> class MyFolder(ExportedFolder): ... folder = resource_dir That view provides the IBrowserPublisher interface necessary to handle all the traversal logic. >>> from zope.interface.verify import verifyObject >>> from zope.publisher.interfaces.browser import IBrowserPublisher >>> from lazr.restful.testing.webservice import FakeRequest >>> view = MyFolder(object(), FakeRequest(version="devel")) >>> verifyObject(IBrowserPublisher, view) True The view will serve the file that it traverses to. >>> view = view.publishTraverse(view.request, 'test.txt') >>> print view() Text file It also sets the appropriate headers for cache control on the response. >>> for name in sorted(view.request.response.headers): ... print "%s: %s" % (name, view.request.response.getHeader(name)) Cache-Control: public... Content-Type: text/plain Expires: ... Last-Modified: ... It accepts traversing to the file through an arbitrary revision identifier. >>> view = MyFolder(object(), FakeRequest(version="devel")) >>> view = view.publishTraverse(view.request, 'rev6510') >>> view = view.publishTraverse(view.request, 'image1.gif') >>> print view() GIF file Requesting a directory raises a NotFound. >>> view = MyFolder(object(), FakeRequest(version="devel")) >>> view = view.publishTraverse(view.request, 'a_dir') >>> view() Traceback (most recent call last): ... NotFound:... By default, subdirectories are not exported. (See below on how to enable this) >>> view = MyFolder(object(), FakeRequest(version="devel")) >>> view = view.publishTraverse(view.request, 'a_dir') >>> view = view.publishTraverse(view.request, 'other.txt') >>> view() Traceback (most recent call last): ... NotFound:... Not requesting any file, also raises NotFound. >>> view = MyFolder(object(), FakeRequest(version="devel")) >>> view() Traceback (most recent call last): ... NotFound:... As requesting a non-existent file. >>> view = MyFolder(object(), FakeRequest(version="devel")) >>> view = view.publishTraverse(view.request, 'image2') >>> view() Traceback (most recent call last): ... NotFound:... == ExportedImageFolder == For images, it's often convenient not to request the extension. There is an ExportedImageFolder subclass, that will accept serving an image file without extension. For example, requesting 'image1' or 'image2' will serve the correct file. The supported extensions are defined in the image_extensions property. >>> from lp.app.browser.folder import ExportedImageFolder >>> class MyImageFolder(ExportedImageFolder): ... folder = resource_dir >>> view = MyImageFolder(object(), FakeRequest(version="devel")) >>> view.image_extensions ('.png', '.gif') >>> view = view.publishTraverse(view.request, 'image2') >>> print view() PNG file >>> print view.request.response.getHeader('Content-Type') image/png If a file without extension exists, that one will be served. >>> file(os.path.join(resource_dir, 'image3'), 'w').write( ... 'Image without extension') >>> file(os.path.join(resource_dir, 'image3.gif'), 'w').write( ... 'Image with extension') >>> view = MyImageFolder(object(), FakeRequest(version="devel")) >>> view = view.publishTraverse(view.request, 'image3') >>> print view() Image without extension >>> view = MyImageFolder(object(), FakeRequest(version="devel")) >>> view = view.publishTraverse(view.request, 'image3.gif') >>> print view() Image with extension == Exporting trees == By default ExportedFolder doesn't export contained folders, but if the export_subdirectories is set to True, it will allow traversing to subdirectories. >>> os.mkdir(os.path.join(resource_dir, 'public')) >>> file(os.path.join( ... resource_dir, 'public', 'test1.txt'), 'w').write('Public File') >>> os.mkdir(os.path.join(resource_dir, 'public', 'subdir1')) >>> file(os.path.join( ... resource_dir, 'public', 'subdir1', 'test1.txt'), 'w').write( ... 'Sub file 1') >>> class MyTree(ExportedFolder): ... folder = resource_dir ... export_subdirectories = True Traversing to a file in a subdirectory will now work. >>> view = MyTree(object(), FakeRequest(version="devel")) >>> view = view.publishTraverse(view.request, 'public') >>> view = view.publishTraverse(view.request, 'subdir1') >>> view = view.publishTraverse(view.request, 'test1.txt') >>> print view() Sub file 1 But traversing to the subdirectory itself will raise a NotFound. >>> view = MyTree(object(), FakeRequest(version="devel")) >>> view = view.publishTraverse(view.request, 'public') >>> print view() Traceback (most recent call last): ... NotFound:... Trying to request a non-existent file, will also raise a NotFound. >>> view = MyTree(object(), FakeRequest(version="devel")) >>> view = view.publishTraverse(view.request, 'public') >>> view = view.publishTraverse(view.request, 'nosuchfile.txt') >>> view() Traceback (most recent call last): ... NotFound:... Traversing beyond an existing file to a non-existant file raises a NotFound. >>> view = MyTree(object(), FakeRequest(version="devel")) >>> view = view.publishTraverse(view.request, 'public') >>> view = view.publishTraverse(view.request, 'subdir1') >>> view = view.publishTraverse(view.request, 'test1.txt') >>> view = view.publishTraverse(view.request, 'nosuchpath') >>> view() Traceback (most recent call last): ... NotFound:... == Clean-up == >>> import shutil >>> shutil.rmtree(resource_dir)