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