1import mimetypes
2import os
3
4from webob import exc
5from webob.dec import wsgify
6from webob.response import Response
7
8__all__ = [
9    'FileApp', 'DirectoryApp',
10]
11
12mimetypes._winreg = None # do not load mimetypes from windows registry
13mimetypes.add_type('text/javascript', '.js') # stdlib default is application/x-javascript
14mimetypes.add_type('image/x-icon', '.ico') # not among defaults
15
16BLOCK_SIZE = 1<<16
17
18
19class 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
66class 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
108class 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