diff --git a/README.md b/README.md index 67766f3..3767a79 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,11 @@ # Run ```bash -uwsgi --plugin /usr/lib/uwsgi/python_plugin.so --http :1180 -w md2html.uwsgi --http-keepalive --http-auto-chunked +uwsgi --need-plugin /usr/lib/uwsgi/python_plugin.so \ + --need-plugin /usr/lib/uwsgi/gevent_plugin.so \ + -H venv \ + --http :1180 \ + -w md2html.uwsgi_handler \ + --http-keepalive \ + --http-auto-chunked \ + --gevent 10 ``` diff --git a/build_docker_image.sh b/build_docker_image.sh index 800d34e..3dffe39 100755 --- a/build_docker_image.sh +++ b/build_docker_image.sh @@ -1,6 +1,10 @@ #!/bin/bash set -e -python3 setup.py bdist_wheel -cp docker/Dockerfile dist/Dockerfile -docker build dist --tag alpine:md2html +venv/bin/python -m build +mkdir -p docker/build +cp dist/md2html-*.whl docker/build/ +cp docker/Dockerfile docker/build/Dockerfile +cp docker/uwsgi.ini docker/build/uwsgi.ini + +docker build docker/build --tag alpine:md2html diff --git a/docker/Dockerfile b/docker/Dockerfile index 3d123d5..3966a7f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,15 +1,20 @@ FROM alpine:latest MAINTAINER Oggioni Walter RUN apk update -RUN apk add python3 py3-pip py3-watchdog uwsgi uwsgi-python3 graphviz +RUN apk add python3 py3-pip uwsgi uwsgi-python3 graphviz uwsgi-gevent3 RUN mkdir /srv/http +RUN mkdir /var/md2html +WORKDIR /var/md2html +RUN python -m venv venv +ADD uwsgi.ini /var/md2html +ADD md2html-*.whl / +RUN venv/bin/pip install /md2html-*.whl && rm /md2html-*.whl VOLUME /srv/http WORKDIR /srv/http -ADD md2html-*.whl / -RUN pip3 install /md2html-*.whl && rm /md2html-*.whl ENTRYPOINT ["uwsgi"] -EXPOSE 1180/tcp -EXPOSE 1180/udp +EXPOSE 1910/tcp +EXPOSE 1910/udp USER nobody -CMD ["--plugin", "/usr/lib/uwsgi/python_plugin.so", "-s", ":1180", "-w", "md2html.uwsgi"] +CMD [ "--ini", "/var/md2html/uwsgi.ini" ] + diff --git a/docker/uwsgi.ini b/docker/uwsgi.ini new file mode 100644 index 0000000..a2f270f --- /dev/null +++ b/docker/uwsgi.ini @@ -0,0 +1,8 @@ +[uwsgi] +#logformat = "%(proto) - %(method) %(uri) %(status) %(addr) +need-plugin=/usr/lib/uwsgi/python_plugin.so +need-plugin=/usr/lib/uwsgi/gevent3_plugin.so +socket = :1910 +module = md2html.uwsgi_handler +virtualenv = /var/md2html/venv +gevent = 1000 \ No newline at end of file diff --git a/md2html/md2html.py b/md2html/md2html.py deleted file mode 100644 index f75f010..0000000 --- a/md2html/md2html.py +++ /dev/null @@ -1,213 +0,0 @@ -#!/usr/bin/env python3 - -import argparse -import hashlib -import sys -import threading -from http.server import BaseHTTPRequestHandler, socketserver, HTTPServer -from os.path import basename, dirname, abspath, join -from urllib.parse import urlparse - -import markdown - -STATIC_CACHE = {} - - -def load_from_cache(path): - global STATIC_CACHE - if path not in STATIC_CACHE: - with open(join(dirname(__file__), 'static') + path, 'r') as static_file: - STATIC_CACHE[path] = static_file.read() - return STATIC_CACHE[path] - - -def compile_html(mdfile=None, extensions=None, raw=None, **kwargs): - html = None - with mdfile and open(mdfile, 'r') or sys.stdin as instream: - html = markdown.markdown(instream.read(), extensions=extensions, output_format='html5') - if raw: - doc = html - else: - css = ' ' % ( - load_from_cache('/github-markdown.css'), - load_from_cache('/custom.css') - ) - doc = load_from_cache('/template.html').format(content=html, script='', css=css) - return doc - - -class ThreadingHTTPServer(socketserver.ThreadingMixIn, HTTPServer): - pass - - -class MarkdownHTTPServer(ThreadingHTTPServer): - - def __init__(self, mdfile, extensions=(), handler=BaseHTTPRequestHandler, interface="127.0.0.1", port=8080): - from watchdog.observers import Observer - from watchdog.events import PatternMatchingEventHandler - import signal - - self.stop = False - - def sigint_handler(signum, frame): - self.stop = True - - handlers = (sigint_handler, signal.getsignal(signal.SIGINT)) - signal.signal(signal.SIGINT, lambda signum, frame: [handler(signum, frame) for handler in handlers]) - - self.mdfile = mdfile - self.extensions = extensions - self.condition_variable = threading.Condition() - self.hash = None - self.etag = None - - event_handler = PatternMatchingEventHandler( - patterns=[mdfile], - ignore_patterns=None, - ignore_directories=True, - case_sensitive=True) - - def on_modified(evt): - self.condition_variable.acquire() - if self.update_file_digest(): - self.condition_variable.notify_all() - self.condition_variable.release() - - event_handler.on_modified = on_modified - - self.observer = Observer() - self.observer.schedule(event_handler, path=dirname(abspath(self.mdfile)), recursive=False) - self.observer.start() - super().__init__((interface, port), handler) - - def update_file_digest(self): - md5 = hashlib.md5() - with open(self.mdfile, 'rb') as mdfile: - md5.update(mdfile.read()) - digest = md5.digest() - if not self.hash or self.hash != digest: - self.hash = digest - self.etag = md5.hexdigest() - return True - else: - return False - - -class MarkdownRequestHandler(BaseHTTPRequestHandler): - status_map = { - 200: "OK", - 204: "No Content", - 304: "Not Modified", - 400: "Bad Request", - 401: "Unauthorized", - 404: "Not Found", - 499: "Service Error", - 500: "Internal Server Error", - 501: "Not Implemented", - 503: "Service Unavailable" - } - - def answer(self, code, reply=None, content_type="text/plain", - headers=()): - output = self.wfile - if not reply: - reply = MarkdownRequestHandler.status_map[code] - try: - self.send_response(code, MarkdownRequestHandler.status_map[code]) - for header in headers: - self.send_header(*header) - self.send_header("Content-Type", content_type) - self.send_header('Content-Length', len(reply)) - self.end_headers() - output.write(reply.encode("UTF-8")) - output.flush() - except BrokenPipeError: - pass - - def markdown_answer(self): - if not self.server.etag: - self.server.condition_variable.acquire() - self.server.update_file_digest() - self.server.condition_variable.release() - self.answer(200, headers=(('Etag', self.server.etag),), - reply=compile_html(mdfile=self.server.mdfile, extensions=self.server.extensions, raw=True), - content_type='text/html') - - def do_GET(self): - path = urlparse(self.path) - if path.path == '/': - self.answer(200, reply=load_from_cache('/template.html').format( - content='', - script='', - css='' - ''), - content_type='text/html') - elif path.path in {'/github-markdown.css', '/custom.css', '/hot-reload.js'}: - self.answer(200, load_from_cache(path.path), content_type='text/css') - elif path.path == '/markdown': - self.markdown_answer() - elif path.path == '/reload': - if 'If-None-Match' not in self.headers or self.headers['If-None-Match'] != self.server.etag: - self.markdown_answer() - else: - self.server.condition_variable.acquire() - self.server.condition_variable.wait(timeout=10) - self.server.condition_variable.release() - if self.server.stop: - self.answer(503) - elif self.headers['If-None-Match'] == self.server.etag: - self.answer(304) - else: - self.answer(200, headers=(('Etag', self.server.etag),), - reply=compile_html(mdfile=self.server.mdfile, - extensions=self.server.extensions, - raw=True), - content_type='text/html') - else: - self.answer(404) - - -def parse_args(args=None): - parser = argparse.ArgumentParser(description='Make a complete, styled HTML document from a Markdown file.') - parser.add_argument('mdfile', help='File to convert. Defaults to stdin.') - parser.add_argument('-o', '--out', help='Output file name. Defaults to stdout.') - parser.add_argument('-r', '--raw', action='store_true', - help='Just output a raw html fragment, as returned from the markdown module') - parser.add_argument('-e', '--extensions', nargs='+', default=['extra', 'smarty', 'tables'], - help='Activate specified markdown extensions (defaults to "extra smarty tables")') - - try: - import watchdog - import signal - parser.add_argument('-w', '--watch', action='store_true', - help='Watch specified source file and rerun the compilation for every time it changes') - parser.add_argument('-p', '--port', default=5000, type=int, - help='Specify http server port (defaults to 5000)') - parser.add_argument('-i', '--interface', default='', - help='Specify http server listen interface (defaults to localhost)') - except ImportError: - pass - return parser.parse_args(args) - - -def write_html(out=None, **kwargs): - doc = compile_html(**kwargs) - with (out and open(out, 'w')) or sys.stdout as outstream: - outstream.write(doc) - - -def main(args=None): - args = parse_args(args) - if hasattr(args, 'watch') and args.watch: - server = MarkdownHTTPServer(args.mdfile, - extensions=args.extensions, - interface=args.interface, - port=args.port, - handler=MarkdownRequestHandler) - server.serve_forever() - else: - write_html(**vars(args)) - - -if __name__ == '__main__': - main() diff --git a/md2html/uwsgi.py b/md2html/uwsgi.py deleted file mode 100644 index a1b1ade..0000000 --- a/md2html/uwsgi.py +++ /dev/null @@ -1,138 +0,0 @@ -import logging -from os import getcwd, listdir -from os.path import exists, splitext, isfile, join, relpath, isdir, basename, getmtime, dirname -from mimetypes import init as mimeinit, guess_type -import hashlib -from .md2html import compile_html -from shutil import which -from subprocess import Popen, PIPE, check_output - -mimeinit() -log = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) - -cwd = getcwd() - - -def has_extension(filepath, extension): - _, ext = splitext(filepath) - return ext == extension - - -def is_markdown(filepath): - return has_extension(filepath, ".md") - - -def is_dotfile(filepath): - return has_extension(filepath, ".dot") - - -cache = dict() - - -def file_hash(filepath, bufsize=4096): - if bufsize <= 0: - raise ValueError("Buffer size must be greater than 0") - md5 = hashlib.md5() - with open(filepath, 'rb') as f: - while True: - buf = f.read(bufsize) - if len(buf) == 0: - break - md5.update(buf) - return md5.digest() - - -def application(env, start_response): - path = join(cwd, relpath(env['PATH_INFO'], '/')) - - if exists(path): - if isfile(path): - cache_result = cache.get(path) - _mtime = None - - def mtime(): - nonlocal _mtime - if not _mtime: - _mtime = getmtime(path) - return _mtime - - if not cache_result or cache_result[1] < mtime(): - digest = file_hash(path).hex() - cache[path] = digest, mtime() - else: - digest = cache_result[0] - - def parse_etag(etag): - if etag is None: - return - start = etag.find('"') - if start < 0: - return - end = etag.find('"', start + 1) - return etag[start + 1: end] - - etag = parse_etag(env.get('HTTP_IF_NONE_MATCH')) - if etag and etag == digest: - start_response('304 Not Modified', [ - ('Etag', '"%s"' % digest), - ('Cache-Control', 'no-cache, must-revalidate, max-age=86400'), - ]) - return [] - elif is_markdown(path): - body = compile_html(path, ['extra', 'smarty', 'tables']).encode() - start_response('200 OK', [('Content-Type', 'text/html; charset=UTF-8'), - ('Etag', '"%s"' % digest), - ('Cache-Control', 'no-cache, must-revalidate, max-age=86400'), - ]) - return [body] - elif is_dotfile(path) and which("dot"): - body = check_output(['dot', '-Tsvg', basename(path)], cwd=dirname(path)) - start_response('200 OK', [('Content-Type', 'image/svg+xml; charset=UTF-8'), - ('Etag', '"%s"' % digest), - ('Cache-Control', 'no-cache, must-revalidate, max-age=86400'), - ]) - return [body] - else: - def read_file(file_path): - buffer_size = 1024 - with open(file_path, 'rb') as f: - while True: - result = f.read(buffer_size) - if len(result) == 0: - break - yield result - - start_response('200 OK', [('Content-Type', guess_type(basename(path))[0] or 'application/octet-stream'), - ('Etag', '"%s"' % digest), - ('Cache-Control', 'no-cache, must-revalidate, max-age=86400'), - ]) - return read_file(path) - elif isdir(path): - body = directory_listing(env['PATH_INFO'], path).encode() - start_response('200 OK', [ - ('Content-Type', 'text/html; charset=UTF-8'), - ]) - return [body] - start_response('404 NOT_FOUND', []) - return [] - - -def directory_listing(path_info, path): - title = "Directory listing for %s" % path_info - result = "" - result += "" - result += "" + title + "" - result += "

" + title + "


" - result += "