1 import mimetypes 2 import os 3 4 from webob import exc 5 from webob.dec import wsgify 6 from webob.response import Response 7 8 __all__ = [ 9 'FileApp', 'DirectoryApp', 10 ] 11 12 mimetypes._winreg = None # do not load mimetypes from windows registry 13 mimetypes.add_type('text/javascript', '.js') # stdlib default is application/x-javascript 14 mimetypes.add_type('image/x-icon', '.ico') # not among defaults 15 16 BLOCK_SIZE = 1<<16 17 18 19 class FileApp(object): 20 """An application that will send the file at the given filename. 21 22 Adds a mime type based on `mimetypes.guess_type()`. 23 """ 24 25 def __init__(self, filename, **kw): 26 self.filename = filename 27 content_type, content_encoding = mimetypes.guess_type(filename) 28 kw.setdefault('content_type', content_type) 29 kw.setdefault('content_encoding', content_encoding) 30 kw.setdefault('accept_ranges', 'bytes') 31 self.kw = kw 32 # Used for testing purpose 33 self._open = open 34 35 @wsgify 36 def __call__(self, req): 37 if req.method not in ('GET', 'HEAD'): 38 return exc.HTTPMethodNotAllowed("You cannot %s a file" % 39 req.method) 40 try: 41 stat = os.stat(self.filename) 42 except (IOError, OSError) as e: 43 msg = "Can't open %r: %s" % (self.filename, e) 44 return exc.HTTPNotFound(comment=msg) 45 46 try: 47 file = self._open(self.filename, 'rb') 48 except (IOError, OSError) as e: 49 msg = "You are not permitted to view this file (%s)" % e 50 return exc.HTTPForbidden(msg) 51 52 if 'wsgi.file_wrapper' in req.environ: 53 app_iter = req.environ['wsgi.file_wrapper'](file, BLOCK_SIZE) 54 else: 55 app_iter = FileIter(file) 56 57 return Response( 58 app_iter = app_iter, 59 content_length = stat.st_size, 60 last_modified = stat.st_mtime, 61 #@@ etag 62 **self.kw 63 ).conditional_response_app 64 65 66 class FileIter(object): 67 def __init__(self, file): 68 self.file = file 69 70 def app_iter_range(self, seek=None, limit=None, block_size=None): 71 """Iter over the content of the file. 72 73 You can set the `seek` parameter to read the file starting from a 74 specific position. 75 76 You can set the `limit` parameter to read the file up to specific 77 position. 78 79 Finally, you can change the number of bytes read at once by setting the 80 `block_size` parameter. 81 """ 82 83 if block_size is None: 84 block_size = BLOCK_SIZE 85 86 if seek: 87 self.file.seek(seek) 88 if limit is not None: 89 limit -= seek 90 try: 91 while True: 92 data = self.file.read(min(block_size, limit) 93 if limit is not None 94 else block_size) 95 if not data: 96 return 97 yield data 98 if limit is not None: 99 limit -= len(data) 100 if limit <= 0: 101 return 102 finally: 103 self.file.close() 104 105 __iter__ = app_iter_range 106 107 108 class DirectoryApp(object): 109 """An application that serves up the files in a given directory. 110 111 This will serve index files (by default ``index.html``), or set 112 ``index_page=None`` to disable this. If you set 113 ``hide_index_with_redirect=True`` (it defaults to False) then 114 requests to, e.g., ``/index.html`` will be redirected to ``/``. 115 116 To customize `FileApp` instances creation (which is what actually 117 serves the responses), override the `make_fileapp` method. 118 """ 119 120 def __init__(self, path, index_page='index.html', hide_index_with_redirect=False, 121 **kw): 122 self.path = os.path.abspath(path) 123 if not self.path.endswith(os.path.sep): 124 self.path += os.path.sep 125 if not os.path.isdir(self.path): 126 raise IOError( 127 "Path does not exist or is not directory: %r" % self.path) 128 self.index_page = index_page 129 self.hide_index_with_redirect = hide_index_with_redirect 130 self.fileapp_kw = kw 131 132 def make_fileapp(self, path): 133 return FileApp(path, **self.fileapp_kw) 134 135 @wsgify 136 def __call__(self, req): 137 path = os.path.abspath(os.path.join(self.path, 138 req.path_info.lstrip('/'))) 139 if os.path.isdir(path) and self.index_page: 140 return self.index(req, path) 141 if (self.index_page and self.hide_index_with_redirect 142 and path.endswith(os.path.sep + self.index_page)): 143 new_url = req.path_url.rsplit('/', 1)[0] 144 new_url += '/' 145 if req.query_string: 146 new_url += '?' + req.query_string 147 return Response( 148 status=301, 149 location=new_url) 150 if not os.path.isfile(path): 151 return exc.HTTPNotFound(comment=path) 152 elif not path.startswith(self.path): 153 return exc.HTTPForbidden() 154 else: 155 return self.make_fileapp(path) 156 157 def index(self, req, path): 158 index_path = os.path.join(path, self.index_page) 159 if not os.path.isfile(index_path): 160 return exc.HTTPNotFound(comment=index_path) 161 if not req.path_info.endswith('/'): 162 url = req.path_url + '/' 163 if req.query_string: 164 url += '?' + req.query_string 165 return Response( 166 status=301, 167 location=url) 168 return self.make_fileapp(index_path) 169