From d962e366df838aac4acabb6d9e0cfbd2890195b1 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Thu, 20 Feb 2014 10:50:41 -0800 Subject: [PATCH] server: Stub out a WSGI server This still needs source-fetching and Content-Range support, but it should handle serving from the cache well enough. --- package_cache/server.py | 95 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 package_cache/server.py diff --git a/package_cache/server.py b/package_cache/server.py new file mode 100644 index 0000000..9fea4a5 --- /dev/null +++ b/package_cache/server.py @@ -0,0 +1,95 @@ +# Copyright + +import email.utils as _email_utils +import mimetypes as _mimetypes +import os as _os +import urllib.parse as _urllib_parse + + +class InvalidFile (ValueError): + def __init__(self, url): + super(InvalidFile, self).__init__('invalid file {!r}'.format(url)) + self.url = url + + +class Server (object): + def __init__(self, sources, cache): + self.sources = sources + self.cache = cache + + def __call__(self, environ, start_response): + try: + return self._serve_request( + environ=environ, start_response=start_response) + except InvalidFile: + start_response(status='404 Not Found', response_headers=[]) + + def _serve_request(self, environ, start_response): + method = environ['REQUEST_METHOD'] + url = environ.get('PATH_INFO', None) + if url is None: + raise InvalidFile(url=url) + parsed_url = _urllib_parse.urlparse(urlstring=url) + relative_path = parsed_url.path.lstrip('/').replace('/', _os.path.sep) + cache_path = _os.path.join(self.cache, relative_path) + if not _os.path.exists(path=cache_path): + self._get_file(url=url, path=cache_path) + if not _os.path.isfile(path=cache_path): + raise InvalidFile(url=url) + return self._serve_file( + path=cache_path, environ=environ, start_response=start_response) + + def _get_file(self, url, path): + raise NotImplementedError() + + def _serve_file(self, path, environ, start_response): + headers = { + 'Content-Length': self._get_content_length(path=path), + 'Content-Type': self._get_content_type(path=path), + 'Last-Modified': self._get_last_modified(path=path), + } + f = open(path, 'rb') + if 'wsgi.file_wrapper' in environ: + file_iterator = environ['wsgi.file_wrapper'](f) + else: + file_iterator = iter(lambda: f.read(block_size), '') + start_response( + status='200 OK', + response_headers=list(headers.items())) + return file_iterator + + def _get_content_length(self, path): + """Content-Length value per RFC 2616 + + Content-Length: + https://tools.ietf.org/html/rfc2616#section-14.13 + """ + return str(_os.path.getsize(path=path)) + + def _get_content_type(self, path): + """Content-Type value per RFC 2616 + + Content-Type: + https://tools.ietf.org/html/rfc2616#section-14.17 + Media types: + https://tools.ietf.org/html/rfc2616#section-3.7 + """ + mimetype, charset = _mimetypes.guess_type(url=path) + if charset: + return '{}; charset={}'.format(mimetype, charset) + else: + return mimetype + + def _get_last_modified(self, path): + """Last-Modified value per RFC 2616 + + Last-Modified: + https://tools.ietf.org/html/rfc2616#section-14.29 + Date formats: + https://tools.ietf.org/html/rfc2616#section-3.3.1 + https://tools.ietf.org/html/rfc1123#page-55 + https://tools.ietf.org/html/rfc822#section-5 + """ + mtime = _os.path.getmtime(path=path) + return _email_utils.formatdate( + timeval=mtime, localtime=False, usegmt=True) -- 2.26.2