#!/usr/bin/env python3 import argparse import binascii import datetime import hashlib import io import json import logging import moth import os import platform import shutil import tempfile import zipfile import random 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 build_category(categorydir, outdir): category_seed = random.getrandbits(32) 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 = int(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) def write_metadata(ziphandle, category): metadata = {"platform": {}, "moth": {}, "category": {}} try: with open("../VERSION", "r") as infile: version = infile.read().strip() metadata["moth"]["version"] = version except IOError: pass metadata["category"]["build_time"] = datetime.datetime.now().strftime("%c") metadata["category"]["type"] = "catmod" if category.catmod is not None else "traditional" metadata["platform"]["arch"] = platform.machine() metadata["platform"]["os"] = platform.system() metadata["platform"]["version"] = platform.platform() metadata["platform"]["python_version"] = platform.python_version() ziphandle.writestr("meta.json", json.dumps(metadata)) # 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", str(seed)) cat = moth.Category(categorydir, seed) mapping = {} answers = {} summary = {} for puzzle in cat: logging.info("Processing point value {}".format(puzzle.points)) hashmap = hashlib.sha1(str(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) for fn, f in puzzle.files.items(): payload = f.stream.read() zf.writestr(os.path.join(puzzledir, fn), payload) obj = puzzle.package() zf.writestr(os.path.join(puzzledir, 'puzzle.json'), json.dumps(obj)) write_kv_pairs(zf, 'map.txt', mapping) write_kv_pairs(zf, 'answers.txt', answers) write_kv_pairs(zf, 'summaries.txt', summary) write_metadata(zf, cat) # 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) outdir = os.path.abspath(args.outdir) for categorydir in args.categorydirs: categorydir = os.path.abspath(categorydir) build_category(categorydir, outdir)