3 import email.utils as _email_utils
4 import mimetypes as _mimetypes
6 import urllib.error as _urllib_error
7 import urllib.request as _urllib_request
9 from . import __version__
12 class InvalidFile (ValueError):
13 def __init__(self, url):
14 super(InvalidFile, self).__init__('invalid file {!r}'.format(url))
18 class Server (object):
19 def __init__(self, sources, cache):
20 self.sources = sources
22 self.opener = _urllib_request.build_opener()
23 self.opener.addheaders = [
24 ('User-agent', 'Package-cache/{}'.format(__version__)),
27 def __call__(self, environ, start_response):
29 return self._serve_request(
30 environ=environ, start_response=start_response)
32 start_response('404 Not Found', [])
33 except _urllib_error.HTTPError as e:
34 print('{} {}'.format(e.code, e.reason))
35 start_response('{} {}'.format(e.code, e.reason), [])
38 def _serve_request(self, environ, start_response):
39 method = environ['REQUEST_METHOD']
40 url = environ.get('PATH_INFO', None)
42 raise InvalidFile(url=url)
43 cache_path = self._get_cache_path(url=url)
44 if not _os.path.exists(path=cache_path):
45 self._get_file_from_sources(url=url, path=cache_path)
46 if not _os.path.isfile(path=cache_path):
47 raise InvalidFile(url=url)
48 return self._serve_file(
49 path=cache_path, environ=environ, start_response=start_response)
51 def _get_cache_path(self, url):
52 relative_path = url.lstrip('/').replace('/', _os.path.sep)
53 cache_path = _os.path.abspath(_os.path.join(self.cache, relative_path))
54 check_relative_path = _os.path.relpath(
55 path=cache_path, start=self.cache)
56 if check_relative_path.startswith(_os.pardir + _os.path.sep):
57 raise InvalidFile(url=url)
60 def _get_file_from_sources(self, url, path):
61 dirname = _os.path.dirname(path)
62 if not _os.path.isdir(dirname):
63 _os.makedirs(dirname, exist_ok=True)
64 for i, source in enumerate(self.sources):
65 source_url = source.rstrip('/') + url
67 self._get_file(url=source_url, path=path)
68 except _urllib_error.HTTPError:
69 if i == len(self.sources) - 1:
74 def _get_file(self, url, path):
75 with self.opener.open(url) as response:
76 content_length = int(response.getheader('Content-Length'))
77 with open(path, 'wb') as f:
80 data = response.read(block_size)
82 if len(data) < block_size:
85 def _serve_file(self, path, environ, start_response):
87 'Content-Length': self._get_content_length(path=path),
88 'Content-Type': self._get_content_type(path=path),
89 'Last-Modified': self._get_last_modified(path=path),
92 if 'wsgi.file_wrapper' in environ:
93 file_iterator = environ['wsgi.file_wrapper'](f)
95 file_iterator = iter(lambda: f.read(block_size), '')
96 start_response('200 OK', list(headers.items()))
99 def _get_content_length(self, path):
100 """Content-Length value per RFC 2616
103 https://tools.ietf.org/html/rfc2616#section-14.13
105 return str(_os.path.getsize(path))
107 def _get_content_type(self, path):
108 """Content-Type value per RFC 2616
111 https://tools.ietf.org/html/rfc2616#section-14.17
113 https://tools.ietf.org/html/rfc2616#section-3.7
115 mimetype, charset = _mimetypes.guess_type(url=path)
117 return '{}; charset={}'.format(mimetype, charset)
121 def _get_last_modified(self, path):
122 """Last-Modified value per RFC 2616
125 https://tools.ietf.org/html/rfc2616#section-14.29
127 https://tools.ietf.org/html/rfc2616#section-3.3.1
128 https://tools.ietf.org/html/rfc1123#page-55
129 https://tools.ietf.org/html/rfc822#section-5
131 mtime = _os.path.getmtime(path)
132 return _email_utils.formatdate(
133 timeval=mtime, localtime=False, usegmt=True)