diff piecrust/serving/server.py @ 553:cc6f3dbe3048

serve: Extract some of the server's functionality into WSGI middlewares.
author Ludovic Chabant <ludovic@chabant.com>
date Sat, 08 Aug 2015 22:01:47 -0700
parents 9612cfc6455a
children 155c7e20414f
line wrap: on
line diff
--- a/piecrust/serving/server.py	Sat Aug 08 16:12:04 2015 -0700
+++ b/piecrust/serving/server.py	Sat Aug 08 22:01:47 2015 -0700
@@ -1,20 +1,18 @@
 import io
 import os
-import re
 import gzip
 import time
 import os.path
 import hashlib
 import logging
-import datetime
 from werkzeug.exceptions import (
         NotFound, MethodNotAllowed, InternalServerError, HTTPException)
 from werkzeug.wrappers import Request, Response
-from werkzeug.wsgi import ClosingIterator, wrap_file
 from jinja2 import FileSystemLoader, Environment
 from piecrust import CACHE_DIR, RESOURCES_DIR
 from piecrust.app import PieCrust
 from piecrust.rendering import QualifiedPage, PageRenderingContext, render_page
+from piecrust.serving.util import content_type_map, make_wrapped_file_response
 from piecrust.sources.base import MODE_PARSING
 from piecrust.uriutil import split_sub_uri
 
@@ -22,6 +20,14 @@
 logger = logging.getLogger(__name__)
 
 
+class WsgiServer(object):
+    def __init__(self, root_dir, **kwargs):
+        self.server = Server(root_dir, **kwargs)
+
+    def __call__(self, environ, start_response):
+        return self.server._run_request(environ, start_response)
+
+
 class ServeRecord(object):
     def __init__(self):
         self.entries = {}
@@ -45,14 +51,6 @@
         self.used_source_names = set()
 
 
-class WsgiServerWrapper(object):
-    def __init__(self, server):
-        self.server = server
-
-    def __call__(self, environ, start_response):
-        return self.server._run_request(environ, start_response)
-
-
 class MultipleNotFound(HTTPException):
     code = 404
 
@@ -73,50 +71,21 @@
 class Server(object):
     def __init__(self, root_dir,
                  debug=False, sub_cache_dir=None, enable_debug_info=True,
-                 static_preview=True, run_sse_check=None):
+                 static_preview=True):
         self.root_dir = root_dir
         self.debug = debug
         self.sub_cache_dir = sub_cache_dir
         self.enable_debug_info = enable_debug_info
-        self.run_sse_check = run_sse_check
         self.static_preview = static_preview
         self._page_record = ServeRecord()
         self._out_dir = os.path.join(root_dir, CACHE_DIR, 'server')
-        self._proc_loop = None
-        self._mimetype_map = load_mimetype_map()
-
-    def getWsgiApp(self):
-        # Bake all the assets so we know what we have, and so we can serve
-        # them to the client. We need a temp app for this.
-        app = PieCrust(root_dir=self.root_dir, debug=self.debug)
-        if self.sub_cache_dir:
-            app._useSubCacheDir(self.sub_cache_dir)
-        self._out_dir = os.path.join(app.sub_cache_dir, 'server')
-
-        if not self.run_sse_check or self.run_sse_check():
-            # When using a server with code reloading, some implementations
-            # use process forking and we end up going here twice. We only want
-            # to start the pipeline loop in the inner process most of the
-            # time so we let the implementation tell us if this is OK.
-            from piecrust.processing.pipeline import ProcessorPipeline
-            from piecrust.serving.procloop import ProcessingLoop
-            pipeline = ProcessorPipeline(app, self._out_dir)
-            self._proc_loop = ProcessingLoop(pipeline)
-            self._proc_loop.start()
-
-        # Run the WSGI app.
-        wsgi_wrapper = WsgiServerWrapper(self)
-        return wsgi_wrapper
+        if sub_cache_dir:
+            self._out_dir = os.path.join(sub_cache_dir, 'server')
 
     def _run_request(self, environ, start_response):
         try:
             response = self._try_run_request(environ)
-            if isinstance(response, tuple):
-                response, close_func = response
-                return ClosingIterator(response(environ, start_response),
-                                       [close_func])
-            else:
-                return response(environ, start_response)
+            return response(environ, start_response)
         except Exception as ex:
             if self.debug:
                 raise
@@ -132,11 +101,6 @@
                          request.method)
             raise MethodNotAllowed()
 
-        # Handle special requests right away.
-        response = self._try_special_request(environ, request)
-        if response is not None:
-            return response
-
         # Also handle requests to a pipeline-built asset right away.
         response = self._try_serve_asset(environ, request)
         if response is not None:
@@ -177,43 +141,6 @@
             msg = "There was an error trying to serve: %s" % request.path
             raise InternalServerError(msg) from ex
 
-    def _try_special_request(self, environ, request):
-        static_mount = '/__piecrust_static/'
-        if request.path.startswith(static_mount):
-            rel_req_path = request.path[len(static_mount):]
-            mount = os.path.join(RESOURCES_DIR, 'server')
-            full_path = os.path.join(mount, rel_req_path)
-            try:
-                response = self._make_wrapped_file_response(
-                        environ, request, full_path)
-                return response
-            except OSError:
-                pass
-
-        debug_mount = '/__piecrust_debug/'
-        if request.path.startswith(debug_mount):
-            rel_req_path = request.path[len(debug_mount):]
-            if rel_req_path == 'werkzeug_shutdown':
-                shutdown_func = environ.get('werkzeug.server.shutdown')
-                if shutdown_func is None:
-                    raise RuntimeError('Not running with the Werkzeug Server')
-                shutdown_func()
-                return Response("Server shutting down...")
-
-            if rel_req_path == 'pipeline_status':
-                from piecrust.serving.procloop import (
-                        PipelineStatusServerSentEventProducer)
-                provider = PipelineStatusServerSentEventProducer(
-                        self._proc_loop)
-                it = provider.run()
-                response = Response(it, mimetype='text/event-stream')
-                response.headers['Cache-Control'] = 'no-cache'
-                response.headers['Last-Event-ID'] = \
-                    self._proc_loop.last_status_id
-                return response, provider.close
-
-        return None
-
     def _try_serve_asset(self, environ, request):
         rel_req_path = request.path.lstrip('/').replace('/', os.sep)
         if request.path.startswith('/_cache/'):
@@ -224,8 +151,7 @@
             full_path = os.path.join(self._out_dir, rel_req_path)
 
         try:
-            response = self._make_wrapped_file_response(
-                    environ, request, full_path)
+            response = make_wrapped_file_response(environ, request, full_path)
             return response
         except OSError:
             pass
@@ -239,7 +165,7 @@
         if not os.path.isfile(full_path):
             return None
 
-        return self._make_wrapped_file_response(environ, request, full_path)
+        return make_wrapped_file_response(environ, request, full_path)
 
     def _try_serve_page(self, app, environ, request):
         # Try to find what matches the requested URL.
@@ -393,28 +319,6 @@
         # Ok all good.
         return rendered_page
 
-    def _make_wrapped_file_response(self, environ, request, path):
-        logger.debug("Serving %s" % path)
-
-        # Check if we can return a 304 status code.
-        mtime = os.path.getmtime(path)
-        etag_str = '%s$$%s' % (path, mtime)
-        etag = hashlib.md5(etag_str.encode('utf8')).hexdigest()
-        if etag in request.if_none_match:
-            response = Response()
-            response.status_code = 304
-            return response
-
-        wrapper = wrap_file(environ, open(path, 'rb'))
-        response = Response(wrapper)
-        _, ext = os.path.splitext(path)
-        response.set_etag(etag)
-        response.last_modified = datetime.datetime.fromtimestamp(mtime)
-        response.mimetype = self._mimetype_map.get(
-                ext.lstrip('.'), 'text/plain')
-        response.direct_passthrough = True
-        return response
-
     def _handle_error(self, exception, environ, start_response):
         code = 500
         if isinstance(exception, HTTPException):
@@ -458,18 +362,6 @@
     pass
 
 
-content_type_map = {
-        'html': 'text/html',
-        'xml': 'text/xml',
-        'txt': 'text/plain',
-        'text': 'text/plain',
-        'css': 'text/css',
-        'xhtml': 'application/xhtml+xml',
-        'atom': 'application/atom+xml',  # or 'text/xml'?
-        'rss': 'application/rss+xml',    # or 'text/xml'?
-        'json': 'application/json'}
-
-
 def find_routes(routes, uri):
     res = []
     tax_res = []
@@ -493,15 +385,3 @@
         return super(ErrorMessageLoader, self).get_source(env, template)
 
 
-def load_mimetype_map():
-    mimetype_map = {}
-    sep_re = re.compile(r'\s+')
-    path = os.path.join(os.path.dirname(__file__), 'mime.types')
-    with open(path, 'r') as f:
-        for line in f:
-            tokens = sep_re.split(line)
-            if len(tokens) > 1:
-                for t in tokens[1:]:
-                    mimetype_map[t] = tokens[0]
-    return mimetype_map
-