diff --git a/devel/devel-server.py b/devel/devel-server.py
index 354f899..4db0b4a 100755
--- a/devel/devel-server.py
+++ b/devel/devel-server.py
@@ -1,6 +1,7 @@
#!/usr/bin/python3
import asyncio
+import cgitb
import glob
import html
from aiohttp import web
@@ -15,11 +16,12 @@ import shutil
import socketserver
import sys
import traceback
+import mothballer
sys.dont_write_bytecode = True # Don't write .pyc files
def mkseed():
- return bytes(random.choice(b'abcdef0123456789') for i in range(40))
+ return bytes(random.choice(b'abcdef0123456789') for i in range(40)).decode('ascii')
class Page:
def __init__(self, title, depth=0):
@@ -72,11 +74,17 @@ async def handle_front(request):
return p.response(request)
async def handle_puzzlelist(request):
+ seed = request.query.get("seed", mkseed())
p = Page("Puzzle Categories", 1)
+ p.write("
seed = {}
".format(seed))
p.write("")
for i in sorted(glob.glob(os.path.join(request.app["puzzles_dir"], "*", ""))):
bn = os.path.basename(i.strip('/\\'))
- p.write('- puzzles/{}/
'.format(bn, bn))
+ p.write("- ")
+ p.write("[mb]".format(cat=bn, seed=seed))
+ p.write(" ")
+ p.write("{cat}".format(cat=bn, seed=seed))
+ p.write("
")
p.write("
")
return p.response(request)
@@ -137,7 +145,7 @@ async def handle_puzzle(request):
return p.response(request)
async def handle_puzzlefile(request):
- seed = request.query.get("seed", mkseed())
+ seed = request.query.get("seed", mkseed()).encode('ascii')
category = request.match_info.get("category")
points = int(request.match_info.get("points"))
filename = request.match_info.get("filename")
@@ -158,6 +166,25 @@ async def handle_puzzlefile(request):
resp.body = file.stream.read()
return resp
+async def handle_mothballer(request):
+ seed = request.query.get("seed", mkseed())
+ category = request.match_info.get("category")
+
+ try:
+ catdir = os.path.join(request.app["puzzles_dir"], category)
+ mb = mothballer.package(category, catdir, seed)
+ except:
+ body = cgitb.html(sys.exc_info())
+ resp = web.Response(text=body, content_type="text/html")
+ return resp
+
+ mb_buf = mb.read()
+ resp = web.Response(
+ body=mb_buf,
+ headers={"Content-Disposition": "attachment; filename={}.mb".format(category)},
+ content_type="application/octet_stream",
+ )
+ return resp
if __name__ == '__main__':
import argparse
@@ -192,5 +219,6 @@ if __name__ == '__main__':
app.router.add_route("GET", "/puzzles/{category}/", handle_category)
app.router.add_route("GET", "/puzzles/{category}/{points}/", handle_puzzle)
app.router.add_route("GET", "/puzzles/{category}/{points}/{filename}", handle_puzzlefile)
+ app.router.add_route("GET", "/mothballer/{category}", handle_mothballer)
app.router.add_static("/files/", mydir, show_index=True)
web.run_app(app, host=addr, port=port)
diff --git a/devel/mothballer.py b/devel/mothballer.py
new file mode 100755
index 0000000..f0799b1
--- /dev/null
+++ b/devel/mothballer.py
@@ -0,0 +1,176 @@
+#!/usr/bin/env python3
+
+import argparse
+import binascii
+import hashlib
+import io
+import json
+import logging
+import moth
+import os
+import shutil
+import tempfile
+import zipfile
+
+SEEDFN = "SEED"
+
+
+def write_kv_pairs(ziphandle, filename, kv):
+ """ Write out a sorted map to file
+ :param ziphandle: a zipfile object
+ :param filename: The filename to write within the zipfile object
+ :param kv: the map to write out
+ :return:
+ """
+ filehandle = io.StringIO()
+ for key in sorted(kv.keys()):
+ if isinstance(kv[key], list):
+ for val in kv[key]:
+ filehandle.write("%s %s\n" % (key, val))
+ else:
+ filehandle.write("%s %s\n" % (key, kv[key]))
+ filehandle.seek(0)
+ ziphandle.writestr(filename, filehandle.read())
+
+
+def escape(s):
+ return s.replace('&', '&').replace('<', '<').replace('>', '>')
+
+
+def generate_html(ziphandle, puzzle, puzzledir, category, points, authors, files):
+ html_content = io.StringIO()
+ file_content = io.StringIO()
+ if files:
+ file_content.write(
+'''
+ Associated files:
+
+''')
+ for fn in files:
+ file_content.write(' - {efn}
\n'.format(fn=fn, efn=escape(fn)))
+ file_content.write(
+'''
+
+''')
+ scripts = [''.format(s) for s in puzzle.scripts]
+
+ html_content.write(
+'''
+
+
+
+
+ {category} {points}
+
+ {scripts}
+
+
+ {category} for {points} points
+
+{file_content}
+ Puzzle by {authors}
+
+'''.format(
+ category=category,
+ points=points,
+ body=puzzle.html_body(),
+ file_content=file_content.getvalue(),
+ authors=', '.join(authors),
+ scripts='\n'.join(scripts),
+ )
+ )
+ ziphandle.writestr(os.path.join(puzzledir, 'index.html'), html_content.getvalue())
+
+
+def build_category(categorydir, outdir):
+ category_seed = binascii.b2a_hex(os.urandom(20))
+
+ categoryname = os.path.basename(categorydir.strip(os.sep))
+ zipfilename = os.path.join(outdir, "%s.mb" % categoryname)
+ logging.info("Building {} from {}".format(zipfilename, categorydir))
+
+ if os.path.exists(zipfilename):
+ # open and gather some state
+ existing = zipfile.ZipFile(zipfilename, 'r')
+ try:
+ category_seed = existing.open(SEEDFN).read().strip()
+ except Exception:
+ pass
+ existing.close()
+ logging.debug("Using PRNG seed {}".format(category_seed))
+
+ zipfileraw = tempfile.NamedTemporaryFile(delete=False)
+ mothball = package(categoryname, categorydir, category_seed)
+ shutil.copyfileobj(mothball, zipfileraw)
+ zipfileraw.close()
+ shutil.move(zipfileraw.name, zipfilename)
+
+
+# Returns a file-like object containing the contents of the new zip file
+def package(categoryname, categorydir, seed):
+ zfraw = io.BytesIO()
+ zf = zipfile.ZipFile(zfraw, 'x')
+ zf.writestr("category_seed.txt", seed)
+
+ cat = moth.Category(categorydir, seed)
+ mapping = {}
+ answers = {}
+ summary = {}
+ for puzzle in cat:
+ logging.info("Processing point value {}".format(puzzle.points))
+
+ hashmap = hashlib.sha1(seed.encode('utf-8'))
+ hashmap.update(str(puzzle.points).encode('utf-8'))
+ puzzlehash = hashmap.hexdigest()
+
+ mapping[puzzle.points] = puzzlehash
+ answers[puzzle.points] = puzzle.answers
+ summary[puzzle.points] = puzzle.summary
+
+ puzzledir = os.path.join('content', puzzlehash)
+ files = []
+ for fn, f in puzzle.files.items():
+ if f.visible:
+ files.append(fn)
+ payload = f.stream.read()
+ zf.writestr(os.path.join(puzzledir, fn), payload)
+
+ puzzledict = {
+ 'authors': puzzle.authors,
+ 'hashes': puzzle.hashes(),
+ 'files': files,
+ 'body': puzzle.html_body(),
+ }
+ puzzlejson = json.dumps(puzzledict)
+ zf.writestr(os.path.join(puzzledir, 'puzzle.json'), puzzlejson)
+ generate_html(zf, puzzle, puzzledir, categoryname, puzzle.points, puzzle.get_authors(), files)
+
+ write_kv_pairs(zf, 'map.txt', mapping)
+ write_kv_pairs(zf, 'answers.txt', answers)
+ write_kv_pairs(zf, 'summaries.txt', summary)
+
+ # clean up
+ zf.close()
+ zfraw.seek(0)
+ return zfraw
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(description='Build a category package')
+ parser.add_argument('outdir', help='Output directory')
+ parser.add_argument('categorydirs', nargs='+', help='Directory of category source')
+ args = parser.parse_args()
+
+ logging.basicConfig(level=logging.DEBUG)
+
+ for categorydir in args.categorydirs:
+ build_category(categorydir, args.outdir)