Home | History | Annotate | Download | only in webob
      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