2016-10-17 16:21:55 -06:00
|
|
|
#!/usr/local/bin/python3
|
2016-10-14 22:26:47 -06:00
|
|
|
|
2016-10-15 21:47:50 -06:00
|
|
|
import glob
|
2016-10-14 22:26:47 -06:00
|
|
|
import http.server
|
|
|
|
import mistune
|
2016-10-16 19:52:09 -06:00
|
|
|
import os
|
2016-10-14 22:26:47 -06:00
|
|
|
import pathlib
|
2016-10-16 20:32:00 -06:00
|
|
|
import puzzles
|
2016-10-14 22:26:47 -06:00
|
|
|
import socketserver
|
|
|
|
|
2016-10-17 19:58:51 -06:00
|
|
|
try:
|
|
|
|
from http.server import HTTPStatus
|
|
|
|
except ImportError:
|
2016-10-17 15:37:11 -06:00
|
|
|
class HTTPStatus:
|
|
|
|
NOT_FOUND = 404
|
|
|
|
OK = 200
|
2016-10-14 22:26:47 -06:00
|
|
|
|
|
|
|
def page(title, body):
|
|
|
|
return """<!DOCTYPE html>
|
|
|
|
<html>
|
|
|
|
<head>
|
|
|
|
<title>{}</title>
|
|
|
|
<link rel="stylesheet" href="/files/www/res/style.css">
|
|
|
|
</head>
|
|
|
|
<body>
|
2016-10-16 20:32:00 -06:00
|
|
|
<div id="preview" class="terminal">
|
2016-10-14 22:26:47 -06:00
|
|
|
{}
|
|
|
|
</div>
|
|
|
|
</body>
|
|
|
|
</html>""".format(title, body)
|
|
|
|
|
2016-10-17 19:58:51 -06:00
|
|
|
|
2016-10-14 22:26:47 -06:00
|
|
|
def mdpage(body):
|
2016-10-15 21:47:50 -06:00
|
|
|
try:
|
|
|
|
title, _ = body.split('\n', 1)
|
|
|
|
except ValueError:
|
|
|
|
title = "Result"
|
|
|
|
title = title.lstrip("#")
|
|
|
|
title = title.strip()
|
2016-10-14 22:26:47 -06:00
|
|
|
return page(title, mistune.markdown(body))
|
|
|
|
|
|
|
|
|
|
|
|
class ThreadingServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
|
|
|
|
pass
|
|
|
|
|
2016-10-17 19:58:51 -06:00
|
|
|
|
2016-10-14 22:26:47 -06:00
|
|
|
class MothHandler(http.server.CGIHTTPRequestHandler):
|
|
|
|
def do_GET(self):
|
|
|
|
if self.path == "/":
|
|
|
|
self.serve_front()
|
2016-10-15 21:47:50 -06:00
|
|
|
elif self.path.startswith("/puzzles"):
|
|
|
|
self.serve_puzzles()
|
|
|
|
elif self.path.startswith("/files"):
|
2016-10-14 22:26:47 -06:00
|
|
|
self.serve_file()
|
|
|
|
else:
|
|
|
|
self.send_error(HTTPStatus.NOT_FOUND, "File not found")
|
|
|
|
|
|
|
|
def translate_path(self, path):
|
|
|
|
if path.startswith('/files'):
|
|
|
|
path = path[7:]
|
|
|
|
return super().translate_path(path)
|
|
|
|
|
|
|
|
def serve_front(self):
|
|
|
|
page = """
|
|
|
|
MOTH Development Server Front Page
|
|
|
|
====================
|
|
|
|
|
|
|
|
Yo, it's the front page.
|
|
|
|
There's stuff you can do here:
|
|
|
|
|
|
|
|
* [Available puzzles](/puzzles)
|
|
|
|
* [Raw filesystem view](/files/)
|
|
|
|
* [Documentation](/files/doc/)
|
|
|
|
* [Instructions](/files/doc/devel-server.md) for using this server
|
|
|
|
|
|
|
|
If you use this development server to run a contest,
|
|
|
|
you are a fool.
|
|
|
|
"""
|
|
|
|
self.serve_md(page)
|
|
|
|
|
2016-10-15 21:47:50 -06:00
|
|
|
def serve_puzzles(self):
|
|
|
|
body = []
|
2016-10-16 20:32:00 -06:00
|
|
|
path = self.path.rstrip('/')
|
|
|
|
parts = path.split("/")
|
2016-10-15 21:47:50 -06:00
|
|
|
if len(parts) < 3:
|
|
|
|
# List all categories
|
2016-10-16 19:52:09 -06:00
|
|
|
body.append("# Puzzle Categories")
|
|
|
|
for i in glob.glob(os.path.join("puzzles", "*", "")):
|
2016-10-15 21:47:50 -06:00
|
|
|
body.append("* [{}](/{})".format(i, i))
|
2016-10-16 20:32:00 -06:00
|
|
|
elif len(parts) == 3:
|
2016-10-16 19:52:09 -06:00
|
|
|
# List all point values in a category
|
|
|
|
body.append("# Puzzles in category `{}`".format(parts[2]))
|
2016-10-16 20:32:00 -06:00
|
|
|
puzz = []
|
|
|
|
for i in glob.glob(os.path.join("puzzles", parts[2], "*.moth")):
|
|
|
|
base = os.path.basename(i)
|
|
|
|
root, _ = os.path.splitext(base)
|
|
|
|
points = int(root)
|
|
|
|
puzz.append(points)
|
|
|
|
for puzzle in sorted(puzz):
|
2016-10-16 19:52:09 -06:00
|
|
|
body.append("* [puzzles/{cat}/{points}](/puzzles/{cat}/{points}/)".format(cat=parts[2], points=puzzle))
|
2016-10-16 20:32:00 -06:00
|
|
|
elif len(parts) == 4:
|
2016-10-16 19:52:09 -06:00
|
|
|
body.append("# {} puzzle {}".format(parts[2], parts[3]))
|
2016-10-17 16:10:41 -06:00
|
|
|
with open("puzzles/{}/{}.moth".format(parts[2], parts[3]), encoding="utf-8") as f:
|
2016-10-16 20:32:00 -06:00
|
|
|
p = puzzles.Puzzle(f)
|
|
|
|
body.append("* Author: `{}`".format(p.fields.get("author")))
|
|
|
|
body.append("* Summary: `{}`".format(p.fields.get("summary")))
|
|
|
|
body.append('')
|
|
|
|
body.append("## Body")
|
|
|
|
body.append(p.body)
|
|
|
|
body.append("## Answers:")
|
|
|
|
for a in p.answers:
|
|
|
|
body.append("* `{}`".format(a))
|
|
|
|
body.append("")
|
2016-10-15 21:47:50 -06:00
|
|
|
else:
|
|
|
|
body.append("# Not Implemented Yet")
|
|
|
|
self.serve_md('\n'.join(body))
|
|
|
|
|
2016-10-14 22:26:47 -06:00
|
|
|
def serve_file(self):
|
|
|
|
if self.path.endswith(".md"):
|
|
|
|
self.serve_md()
|
|
|
|
else:
|
|
|
|
super().do_GET()
|
|
|
|
|
|
|
|
def serve_md(self, text=None):
|
|
|
|
fspathstr = self.translate_path(self.path)
|
|
|
|
fspath = pathlib.Path(fspathstr)
|
|
|
|
if not text:
|
|
|
|
try:
|
|
|
|
text = fspath.read_text()
|
|
|
|
except OSError:
|
|
|
|
self.send_error(HTTPStatus.NOT_FOUND, "File not found")
|
|
|
|
return None
|
|
|
|
content = mdpage(text)
|
|
|
|
|
2016-10-17 15:37:11 -06:00
|
|
|
self.send_response(HTTPStatus.OK)
|
2016-10-17 16:21:55 -06:00
|
|
|
|
2016-10-17 16:10:41 -06:00
|
|
|
self.send_header("Content-type", "text/html; charset=utf-8")
|
2016-10-14 22:26:47 -06:00
|
|
|
self.send_header("Content-Length", len(content))
|
|
|
|
try:
|
|
|
|
fs = fspath.stat()
|
|
|
|
self.send_header("Last-Modified", self.date_time_string(fs.st_mtime))
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
self.end_headers()
|
|
|
|
self.wfile.write(content.encode('utf-8'))
|
|
|
|
|
2016-10-17 19:58:51 -06:00
|
|
|
|
2016-10-14 22:26:47 -06:00
|
|
|
def run(address=('', 8080)):
|
|
|
|
httpd = ThreadingServer(address, MothHandler)
|
|
|
|
print("=== Listening on http://{}:{}/".format(address[0] or "localhost", address[1]))
|
|
|
|
httpd.serve_forever()
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
run()
|