paulb@386 | 1 | #!/usr/bin/env python |
paulb@386 | 2 | |
paulb@386 | 3 | """ |
paulb@386 | 4 | Resources for serving static content. |
paulb@403 | 5 | |
paulb@563 | 6 | Copyright (C) 2004, 2005, 2006 Paul Boddie <paul@boddie.org.uk> |
paulb@403 | 7 | |
paulb@403 | 8 | This library is free software; you can redistribute it and/or |
paulb@403 | 9 | modify it under the terms of the GNU Lesser General Public |
paulb@403 | 10 | License as published by the Free Software Foundation; either |
paulb@403 | 11 | version 2.1 of the License, or (at your option) any later version. |
paulb@403 | 12 | |
paulb@403 | 13 | This library is distributed in the hope that it will be useful, |
paulb@403 | 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
paulb@403 | 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
paulb@403 | 16 | Lesser General Public License for more details. |
paulb@403 | 17 | |
paulb@403 | 18 | You should have received a copy of the GNU Lesser General Public |
paulb@403 | 19 | License along with this library; if not, write to the Free Software |
paulb@489 | 20 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA |
paulb@386 | 21 | """ |
paulb@386 | 22 | |
paulb@386 | 23 | from WebStack.Generic import ContentType, EndOfResponse |
paulb@386 | 24 | import os |
paulb@386 | 25 | |
paulb@386 | 26 | class DirectoryResource: |
paulb@386 | 27 | |
paulb@386 | 28 | "A resource serving the contents of a filesystem directory." |
paulb@386 | 29 | |
paulb@563 | 30 | def __init__(self, directory, media_types=None, |
paulb@563 | 31 | unrecognised_media_type="application/data", content_types=None, |
paulb@563 | 32 | unrecognised_content_type=None, default_encoding=None, |
paulb@563 | 33 | urlencoding="utf-8"): |
paulb@386 | 34 | |
paulb@386 | 35 | """ |
paulb@386 | 36 | Initialise the resource to serve files from the given 'directory'. |
paulb@386 | 37 | |
paulb@563 | 38 | The optional 'content_types' dictionary can be used to map filename |
paulb@563 | 39 | extensions to content types, where extensions consist of the part of a |
paulb@563 | 40 | name after a "." character (such as "txt", "html"), and where content |
paulb@563 | 41 | types are typically WebStack.Generic.ContentType objects. |
paulb@563 | 42 | |
paulb@386 | 43 | The optional 'media_types' dictionary can be used to map filename |
paulb@386 | 44 | extensions to media types, where extensions consist of the part of a |
paulb@386 | 45 | name after a "." character (such as "txt", "html"), and where media |
paulb@386 | 46 | types are the usual content descriptions (such as "text/plain" and |
paulb@386 | 47 | "text/html"). |
paulb@386 | 48 | |
paulb@563 | 49 | If 'content_types' or 'media_types' contain a mapping from None to a |
paulb@563 | 50 | content or media type, then this mapping is used when no extension is |
paulb@563 | 51 | present on a requested resource name. |
paulb@386 | 52 | |
paulb@563 | 53 | Where no content or media type can be found for a resource, a |
paulb@563 | 54 | predefined media type is set which can be overridden by specifying a |
paulb@563 | 55 | value for the optional 'unrecognised_media_type' or for the |
paulb@563 | 56 | 'unrecognised_content_type' parameter - the latter overriding the former |
paulb@563 | 57 | if specified. |
paulb@563 | 58 | |
paulb@563 | 59 | The optional 'default_encoding' is used to specify the character |
paulb@563 | 60 | encoding used in any content type produced from a media type (or for |
paulb@563 | 61 | the unrecognised media type). If set to None (as is the default), no |
paulb@563 | 62 | encoding declaration is produced for file content associated with media |
paulb@563 | 63 | types. |
paulb@446 | 64 | |
paulb@446 | 65 | The optional 'urlencoding' is used to decode "URL encoded" character |
paulb@446 | 66 | values in the request path, and overrides the default encoding wherever |
paulb@446 | 67 | possible. |
paulb@386 | 68 | """ |
paulb@386 | 69 | |
paulb@386 | 70 | self.directory = directory |
paulb@563 | 71 | self.content_types = content_types or {} |
paulb@386 | 72 | self.media_types = media_types or {} |
paulb@386 | 73 | self.unrecognised_media_type = unrecognised_media_type |
paulb@563 | 74 | self.unrecognised_content_type = unrecognised_content_type |
paulb@563 | 75 | self.default_encoding = default_encoding |
paulb@446 | 76 | self.urlencoding = urlencoding |
paulb@386 | 77 | |
paulb@386 | 78 | def respond(self, trans): |
paulb@386 | 79 | |
paulb@386 | 80 | "Respond to the given transaction, 'trans', by serving a file." |
paulb@386 | 81 | |
paulb@446 | 82 | parts = trans.get_virtual_path_info(self.urlencoding).split("/") |
paulb@386 | 83 | filename = parts[1] |
paulb@386 | 84 | out = trans.get_response_stream() |
paulb@386 | 85 | |
paulb@386 | 86 | # Test for the file's existence. |
paulb@386 | 87 | |
paulb@387 | 88 | pathname = os.path.abspath(os.path.join(self.directory, filename)) |
paulb@563 | 89 | if not (pathname.startswith(os.path.join(self.directory, "/")) and \ |
paulb@563 | 90 | os.path.exists(pathname) and os.path.isfile(pathname)): |
paulb@563 | 91 | |
paulb@512 | 92 | self.not_found(trans, filename) |
paulb@386 | 93 | |
paulb@386 | 94 | # Get the extension. |
paulb@386 | 95 | |
paulb@386 | 96 | extension_parts = filename.split(".") |
paulb@386 | 97 | |
paulb@386 | 98 | if len(extension_parts) > 1: |
paulb@386 | 99 | extension = extension_parts[-1] |
paulb@563 | 100 | content_type = self.content_types.get(extension) |
paulb@386 | 101 | media_type = self.media_types.get(extension) |
paulb@386 | 102 | else: |
paulb@563 | 103 | content_type = self.content_types.get(None) |
paulb@386 | 104 | media_type = self.media_types.get(None) |
paulb@386 | 105 | |
paulb@386 | 106 | # Set the content type. |
paulb@386 | 107 | |
paulb@563 | 108 | if content_type is not None: |
paulb@563 | 109 | trans.set_content_type(content_type) |
paulb@563 | 110 | elif media_type is not None: |
paulb@563 | 111 | trans.set_content_type(ContentType(media_type, self.default_encoding)) |
paulb@563 | 112 | elif self.unrecognised_content_type is not None: |
paulb@563 | 113 | trans.set_content_type(self.unrecognised_content_type) |
paulb@386 | 114 | else: |
paulb@563 | 115 | trans.set_content_type( |
paulb@563 | 116 | ContentType(self.unrecognised_media_type, self.default_encoding)) |
paulb@386 | 117 | |
paulb@386 | 118 | # Write the file to the client. |
paulb@386 | 119 | |
paulb@386 | 120 | f = open(os.path.join(self.directory, filename), "rb") |
paulb@386 | 121 | out.write(f.read()) |
paulb@386 | 122 | f.close() |
paulb@386 | 123 | |
paulb@512 | 124 | def not_found(self, trans, filename): |
paulb@512 | 125 | |
paulb@512 | 126 | """ |
paulb@512 | 127 | Send the "not found" response using the given transaction, 'trans', and |
paulb@512 | 128 | specifying the given 'filename' (if appropriate). |
paulb@512 | 129 | """ |
paulb@512 | 130 | |
paulb@512 | 131 | trans.set_response_code(404) |
paulb@512 | 132 | trans.set_content_type(ContentType("text/plain")) |
paulb@512 | 133 | out = trans.get_response_stream() |
paulb@512 | 134 | out.write("Resource '%s' not found." % filename) |
paulb@512 | 135 | raise EndOfResponse |
paulb@512 | 136 | |
paulb@545 | 137 | class FileResource: |
paulb@545 | 138 | |
paulb@545 | 139 | "A file serving resource." |
paulb@545 | 140 | |
paulb@545 | 141 | def __init__(self, filename, content_type): |
paulb@545 | 142 | self.filename = filename |
paulb@545 | 143 | self.content_type = content_type |
paulb@545 | 144 | |
paulb@545 | 145 | def respond(self, trans): |
paulb@574 | 146 | trans.set_content_type(self.content_type) |
paulb@545 | 147 | f = open(self.filename, "rb") |
paulb@545 | 148 | trans.get_response_stream().write(f.read()) |
paulb@545 | 149 | f.close() |
paulb@545 | 150 | |
paulb@386 | 151 | # vim: tabstop=4 expandtab shiftwidth=4 |