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 try: 26 from email.utils import formatdate 27 except ImportError: 28 from email.Utils import formatdate as _formatdate 29 def formatdate(timeval=None, localtime=False, usegmt=False): 30 s = _formatdate(timeval, localtime) 31 l = s.split(" ") # get components 32 if usegmt: 33 del l[-1] # remove time-zone offset 34 l.append("GMT") # add mandatory name 35 return " ".join(l) 36 37 class DirectoryResource: 38 39 "A resource serving the contents of a filesystem directory." 40 41 def __init__(self, directory, media_types=None, 42 unrecognised_media_type="application/data", content_types=None, 43 unrecognised_content_type=None, default_encoding=None, 44 urlencoding=None): 45 46 """ 47 Initialise the resource to serve files from the given 'directory'. 48 49 The optional 'content_types' dictionary can be used to map filename 50 extensions to content types, where extensions consist of the part of a 51 name after a "." character (such as "txt", "html"), and where content 52 types are typically WebStack.Generic.ContentType objects. 53 54 The optional 'media_types' dictionary can be used to map filename 55 extensions to media types, where extensions consist of the part of a 56 name after a "." character (such as "txt", "html"), and where media 57 types are the usual content descriptions (such as "text/plain" and 58 "text/html"). 59 60 If 'content_types' or 'media_types' contain a mapping from None to a 61 content or media type, then this mapping is used when no extension is 62 present on a requested resource name. 63 64 Where no content or media type can be found for a resource, a 65 predefined media type is set which can be overridden by specifying a 66 value for the optional 'unrecognised_media_type' or for the 67 'unrecognised_content_type' parameter - the latter overriding the former 68 if specified. 69 70 The optional 'default_encoding' is used to specify the character 71 encoding used in any content type produced from a media type (or for 72 the unrecognised media type). If set to None (as is the default), no 73 encoding declaration is produced for file content associated with media 74 types. 75 76 The optional 'urlencoding' is used to decode "URL encoded" character 77 values in the request path, and overrides the default encoding wherever 78 possible. 79 """ 80 81 self.directory = directory 82 self.content_types = content_types or {} 83 self.media_types = media_types or {} 84 self.unrecognised_media_type = unrecognised_media_type 85 self.unrecognised_content_type = unrecognised_content_type 86 self.default_encoding = default_encoding 87 self.urlencoding = urlencoding 88 89 def respond(self, trans): 90 91 "Respond to the given transaction, 'trans', by serving a file." 92 93 parts = trans.get_virtual_path_info(self.urlencoding).split("/") 94 filename = parts[1] 95 out = trans.get_response_stream() 96 97 # Test for the file's existence. 98 99 pathname = os.path.abspath(os.path.join(self.directory, filename)) 100 if not (pathname.startswith(os.path.join(self.directory, "/")) and \ 101 os.path.exists(pathname) and os.path.isfile(pathname)): 102 103 self.not_found(trans, filename) 104 105 # Get the extension. 106 107 extension_parts = filename.split(".") 108 109 if len(extension_parts) > 1: 110 extension = extension_parts[-1] 111 content_type = self.content_types.get(extension) 112 media_type = self.media_types.get(extension) 113 else: 114 content_type = self.content_types.get(None) 115 media_type = self.media_types.get(None) 116 117 # Set the content type. 118 119 if content_type is not None: 120 trans.set_content_type(content_type) 121 elif media_type is not None: 122 trans.set_content_type(ContentType(media_type, self.default_encoding)) 123 elif self.unrecognised_content_type is not None: 124 trans.set_content_type(self.unrecognised_content_type) 125 else: 126 trans.set_content_type( 127 ContentType(self.unrecognised_media_type, self.default_encoding)) 128 129 # Write the file to the client. 130 131 pathname = os.path.join(self.directory, filename) 132 mtime = formatdate(os.path.getmtime(pathname), usegmt=1) 133 trans.set_header_value("Last-Modified", mtime) 134 135 f = open(pathname, "rb") 136 out.write(f.read()) 137 f.close() 138 139 def not_found(self, trans, filename): 140 141 """ 142 Send the "not found" response using the given transaction, 'trans', and 143 specifying the given 'filename' (if appropriate). 144 """ 145 146 trans.set_response_code(404) 147 trans.set_content_type(ContentType("text/plain")) 148 out = trans.get_response_stream() 149 out.write("Resource '%s' not found." % filename) 150 raise EndOfResponse 151 152 class FileResource: 153 154 "A file serving resource." 155 156 def __init__(self, filename, content_type): 157 self.filename = filename 158 self.content_type = content_type 159 160 def respond(self, trans): 161 mtime = formatdate(os.path.getmtime(self.filename), usegmt=1) 162 163 trans.set_content_type(self.content_type) 164 trans.set_header_value("Last-Modified", mtime) 165 f = open(self.filename, "rb") 166 trans.get_response_stream().write(f.read()) 167 f.close() 168 169 class StringResource: 170 171 "A resource serving a string as a page." 172 173 def __init__(self, s, content_type): 174 self.s = s 175 self.content_type = content_type 176 177 def respond(self, trans): 178 trans.set_content_type(self.content_type) 179 trans.get_response_stream().write(self.s) 180 181 # vim: tabstop=4 expandtab shiftwidth=4