3 import email.utils as _email_utils
4 import mimetypes as _mimetypes
6 import urllib.parse as _urllib_parse
9 class InvalidFile (ValueError):
10 def __init__(self, url):
11 super(InvalidFile, self).__init__('invalid file {!r}'.format(url))
15 class Server (object):
16 def __init__(self, sources, cache):
17 self.sources = sources
19 if not _os.path.isdir(self.cache):
20 _os.makedirs(path=self.cache, exist_ok=True)
22 def __call__(self, environ, start_response):
24 return self._serve_request(
25 environ=environ, start_response=start_response)
27 start_response(status='404 Not Found', response_headers=[])
29 def _serve_request(self, environ, start_response):
30 method = environ['REQUEST_METHOD']
31 url = environ.get('PATH_INFO', None)
33 raise InvalidFile(url=url)
34 parsed_url = _urllib_parse.urlparse(urlstring=url)
35 relative_path = parsed_url.path.lstrip('/').replace('/', _os.path.sep)
36 cache_path = _os.path.join(self.cache, relative_path)
37 if not _os.path.exists(path=cache_path):
38 self._get_file(url=url, path=cache_path)
39 if not _os.path.isfile(path=cache_path):
40 raise InvalidFile(url=url)
41 return self._serve_file(
42 path=cache_path, environ=environ, start_response=start_response)
44 def _get_file(self, url, path):
45 raise NotImplementedError()
47 def _serve_file(self, path, environ, start_response):
49 'Content-Length': self._get_content_length(path=path),
50 'Content-Type': self._get_content_type(path=path),
51 'Last-Modified': self._get_last_modified(path=path),
54 if 'wsgi.file_wrapper' in environ:
55 file_iterator = environ['wsgi.file_wrapper'](f)
57 file_iterator = iter(lambda: f.read(block_size), '')
60 response_headers=list(headers.items()))
63 def _get_content_length(self, path):
64 """Content-Length value per RFC 2616
67 https://tools.ietf.org/html/rfc2616#section-14.13
69 return str(_os.path.getsize(path=path))
71 def _get_content_type(self, path):
72 """Content-Type value per RFC 2616
75 https://tools.ietf.org/html/rfc2616#section-14.17
77 https://tools.ietf.org/html/rfc2616#section-3.7
79 mimetype, charset = _mimetypes.guess_type(url=path)
81 return '{}; charset={}'.format(mimetype, charset)
85 def _get_last_modified(self, path):
86 """Last-Modified value per RFC 2616
89 https://tools.ietf.org/html/rfc2616#section-14.29
91 https://tools.ietf.org/html/rfc2616#section-3.3.1
92 https://tools.ietf.org/html/rfc1123#page-55
93 https://tools.ietf.org/html/rfc822#section-5
95 mtime = _os.path.getmtime(path=path)
96 return _email_utils.formatdate(
97 timeval=mtime, localtime=False, usegmt=True)