gallery.py: Convert '--scgi' to '--mode=scgi' and add a 'wsgi' mode
authorW. Trevor King <wking@tremily.us>
Mon, 9 Dec 2013 05:21:21 +0000 (21:21 -0800)
committerW. Trevor King <wking@tremily.us>
Mon, 9 Dec 2013 05:21:21 +0000 (21:21 -0800)
This is useful for serving a one-off gallery when you don't want to
bother setting up a real frontend for the CGI servers.  We use the
ProcessingComplete exception to pass out the header OrderedDict for
start_response.

posts/gallery/gallery.py

index ac62c3686cece39500128bc8cb62e08e4bb12d17..110f383c8908708cc73925fa3f2f473c6e9e2a0c 100755 (executable)
@@ -15,8 +15,7 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-"""
-CGI gallery server for a picture directory organized along::
+"""Gallery server for a picture directory organized along::
 
   pics
   |-- some_directory
@@ -36,10 +35,12 @@ With::
 Note that you can store a caption for ``<PICTURE>`` as plain text in
 ``<PICTURE>.txt``.
 
-See RFC 3875 for more details on the the Common Gateway Interface.
+See RFC 3875 for more details on the the Common Gateway Interface (CGI).
+
+Besides the CGI interface, this script can also be run as:
 
-This script can also be run as a Simple Common Gateway Interface
-(SCGI) with the ``--scgi`` option.
+* a Simple Common Gateway Interface (SCGI) with the ``--mode=scgi`` option
+* a stand-alone server with the ``--mode=wsgi`` option
 """
 
 import collections as _collections
@@ -105,7 +106,8 @@ class HTTPError(Exception):
 
 
 class ProcessingComplete(Exception):
-    pass
+    def __init__(self, headers=None):
+        self.headers = headers
 
 
 def invoke(args, stdin=None, stdout=_subprocess.PIPE, stderr=_subprocess.PIPE,
@@ -153,11 +155,13 @@ class CGIGalleryServer (object):
     def __init__(self, base_path='.',
                  base_url='/',
                  cache_path='/tmp/gallery-cache/',
-                 serve_originals=True):
+                 serve_originals=True,
+                 write_http_headers=True):
         self._base_path = _os_path.abspath(base_path)
         self._base_url = base_url
         self._cache_path = cache_path
         self._serve_originals = serve_originals
+        self._write_http_headers = write_http_headers
         self._text_charset = 'UTF-8'
         self._url_regexp = _re.compile('^[a-zA-Z0-9._/-]*$')
         self._rows = 3
@@ -197,7 +201,7 @@ class CGIGalleryServer (object):
         if charset:
             content = content.encode(charset)
         stream.write(content)
-        raise ProcessingComplete()
+        raise ProcessingComplete(headers=headers)
 
     def _response_stream(self, headers=None, content=None, stream=None,
                          chunk_size=1024):
@@ -216,7 +220,7 @@ class CGIGalleryServer (object):
             if not chunk:
                 break
             stream.write(chunk)
-        raise ProcessingComplete()
+        raise ProcessingComplete(headers=headers)
 
     def _error(self, status=404, content=None, stream=None):
         headers = self._http_headers(status=status)
@@ -264,8 +268,8 @@ class CGIGalleryServer (object):
             except HTTPError as e:
                 LOG.error(e.message)
                 self._error(e.status, content=e.content, stream=stream)
-        except ProcessingComplete:
-            pass
+        except ProcessingComplete as e:
+            return e
 
     def page_from_query(self, query=None, query_string=None):
         """Extract the requested page from a query string
@@ -776,6 +780,39 @@ def serve_scgi(server, host='localhost', port=4000):
     LOG.info('serving SCGI on {}:{}'.format(host, port))
     s.serve()
 
+def serve_wsgi(server, host='localhost', port=4000):
+    import io
+    import wsgiref.simple_server
+
+    server._write_http_headers = False
+
+    def app(environ, start_response):
+        url = environ.get('PATH_INFO', None)
+        page = server.page_from_query(
+            query_string=environ.get('QUERY_STRING', ''))
+        status = '200 OK'
+        headers = {}
+        stream = io.BytesIO()
+        try:
+            try:
+                url = server.relative_url(url=url)
+            except HTTPError as e:
+                LOG.error(e.message)
+                server._error(e.status, content=e.content, stream=stream)
+        except ProcessingComplete as e:
+            headers = e.headers
+        else:
+            e = server.serve(url=url, page=page, stream=stream)
+            headers = e.headers
+        output = stream.getvalue()
+        status = headers.pop('Status')
+        start_response(status, list(headers.items()))
+        return [output]
+
+    wsgi = wsgiref.simple_server.make_server(host=host, port=port, app=app)
+    LOG.info('serving WSGI on {}:{}'.format(host, port))
+    wsgi.serve_forever()
+
 
 if __name__ == '__main__':
     import argparse as _argparse
@@ -784,8 +821,8 @@ if __name__ == '__main__':
         description=__doc__, version=__version__,
         formatter_class=_argparse.RawDescriptionHelpFormatter)
     parser.add_argument(
-        '--scgi', default=False, action='store_const', const=True,
-        help='Run as a SCGI server (vs. serving a single CGI call)')
+        '--mode', default='cgi', choices=['cgi', 'scgi', 'wsgi'],
+        help='Server mode (defaults to CGI)')
     parser.add_argument(
         '--port', default=4000, type=int,
         help='Port to listen to (if runing as a SCGI server)')
@@ -813,7 +850,9 @@ if __name__ == '__main__':
         s.header = [open(_os_path.join(shared, 'header.shtml'), 'r').read()]
         s.footer = [open(_os_path.join(shared, 'footer.shtml'), 'r').read()]
 
-    if args.scgi:
+    if args.mode == 'scgi':
         serve_scgi(server=s, port=args.port)
+    if args.mode == 'wsgi':
+        serve_wsgi(server=s, port=args.port)
     else:
         serve_cgi(server=s)