diff --git a/devel-server.py b/devel-server.py index 5543f83..10133e5 100755 --- a/devel-server.py +++ b/devel-server.py @@ -102,35 +102,92 @@ you are a fool. body = [] path = self.path.rstrip('/') parts = path.split("/") + #raise ValueError(parts) if len(parts) < 3: # List all categories body.append("# Puzzle Categories") for i in glob.glob(os.path.join("puzzles", "*", "")): body.append("* [{}](/{})".format(i, i)) - elif len(parts) == 3: + self.serve_md('\n'.join(body)) + return + + fpath = os.path.join("puzzles", parts[2]) + cat = puzzles.Category(fpath, seed) + if len(parts) == 3: # List all point values in a category body.append("# Puzzles in category `{}`".format(parts[2])) - fpath = os.path.join("puzzles", parts[2]) - cat = puzzles.Category(fpath, seed) for points in cat.pointvals: body.append("* [puzzles/{cat}/{points}](/puzzles/{cat}/{points}/)".format(cat=parts[2], points=points)) - elif len(parts) == 4: + self.serve_md('\n'.join(body)) + return + + pzl = cat.puzzle(int(parts[3])) + if len(parts) == 4: body.append("# {} puzzle {}".format(parts[2], parts[3])) - fpath = os.path.join("puzzles", parts[2]) - cat = puzzles.Category(fpath, seed) - p = cat.puzzle(int(parts[3])) - body.append("* Author: `{}`".format(p['author'])) - body.append("* Summary: `{}`".format(p['summary'])) + body.append("* Author: `{}`".format(pzl['author'])) + body.append("* Summary: `{}`".format(pzl['summary'])) body.append('') body.append("## Body") - body.append(p.body) + body.append(pzl.body) body.append("## Answers") - for a in p['answers']: + for a in pzl['answer']: body.append("* `{}`".format(a)) body.append("") + body.append("## Files") + for pzl_file in pzl['files']: + body.append("* [puzzles/{cat}/{points}/{filename}]({filename})" + .format(cat=parts[2], points=pzl.points, filename=pzl_file)) + + if len(pzl.logs) > 0: + body.extend(["", "## Logs"]) + body.append("* [Full Log File](_logs)" + .format(cat=parts[2], points=pzl.points)) + body.extend(["", "### Logs Head"]) + for log in pzl.logs[:10]: + body.append("* `{}`".format(log)) + body.extend(["", "### Logs Tail"]) + for log in pzl.logs[-10:]: + body.append("* `{}`".format(log)) + self.serve_md('\n'.join(body)) + return + elif len(parts) == 5: + if parts[4] == '_logs': + self.serve_puzzle_logs(pzl.logs) + else: + try: + self.serve_puzzle_file(pzl['files'][parts[4]]) + except KeyError: + self.send_error(HTTPStatus.NOT_FOUND, "File not found") + return else: body.append("# Not Implemented Yet") - self.serve_md('\n'.join(body)) + self.serve_md('\n'.join(body)) + + def serve_puzzle_logs(self, logs): + """Serve a PuzzleFile object.""" + self.send_response(HTTPStatus.OK) + self.send_header("Content-type", "text/plain; charset=utf-8") + self.end_headers() + for log in logs: + self.wfile.write(log.encode('ascii')) + self.wfile.write(b"\n") + + CHUNK_SIZE = 4096 + def serve_puzzle_file(self, file): + """Serve a PuzzleFile object.""" + self.send_response(HTTPStatus.OK) + self.send_header("Content-type", "application/octet-stream") + self.send_header('Content-Disposition', 'attachment; filename="{}"'.format(file.name)) + if file.path is not None: + fs = os.stat(file.path) + self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) + + # We're using application/octet stream, so we can send the raw bytes. + self.end_headers() + chunk = file.handle.read(self.CHUNK_SIZE) + while chunk: + self.wfile.write(chunk) + chunk = file.handle.read(self.CHUNK_SIZE) def serve_file(self): if self.path.endswith(".md"): diff --git a/puzzles.py b/puzzles.py index 457a168..adb929f 100644 --- a/puzzles.py +++ b/puzzles.py @@ -88,12 +88,15 @@ class Puzzle: self._dict = defaultdict(lambda: []) if os.path.isdir(path): - self._puzzle_dir = path + self.puzzle_dir = path else: - self._puzzle_dir = None + self.puzzle_dir = None self.message = bytes(random.choice(messageChars) for i in range(20)) self.body = '' + # This defaults to a dict, not a list like most things + self._dict['files'] = {} + # A list of temporary files we've created that will need to be deleted. self._temp_files = [] if path is not None: @@ -111,6 +114,8 @@ class Puzzle: self._seed = category_seed * self.points self.rand = random.Random(self._seed) + self._logs = [] + if path is not None: files = os.listdir(path) @@ -136,6 +141,25 @@ class Puzzle: except OSError: pass + def log(self, msg): + """Add a new log message to this puzzle.""" + self._logs.append(msg) + + @property + def logs(self): + """Get all the log messages, as strings.""" + + _logs = [] + for log in self._logs: + if type(log) is bytes: + log = log.decode('utf-8') + elif type(log) is not str: + log = str(log) + + _logs.append(log) + + return _logs + def _read_config(self, stream): """Read a configuration file (ISO 2822)""" body = [] @@ -171,18 +195,17 @@ class Puzzle: # Make sure it actually exists. if not os.path.exists(path): - raise ValueError("Included file {} does not exist.") + raise ValueError("Included file {} does not exist.".format(path)) file = open(path, 'rb') return PuzzleFile(path=path, handle=file, name=name, visible=visible) - def make_temp_file(self, name=None, mode='rw+b', visible=True): + def make_temp_file(self, name=None, visible=True): """Get a file object for adding dynamically generated data to the puzzle. When you're done with this file, flush it, but don't close it. :param name: The name of the file for links within the puzzle. If this is None, a name will be generated for you. - :param mode: The mode under which :param visible: Whether or not the file will be visible to the user. :return: A file object for writing """ @@ -190,7 +213,7 @@ class Puzzle: if name is None: name = self.random_hash() - file = tempfile.NamedTemporaryFile(mode=mode, delete=False) + file = tempfile.NamedTemporaryFile(mode='w+b', delete=False) file_read = open(file.name, 'rb') self._dict['files'][name] = PuzzleFile(path=file.name, handle=file_read, @@ -218,7 +241,7 @@ class Puzzle: key = key.lower() - if key in ('file', 'resource', 'hidden') and self._puzzle_dir is None: + if key in ('file', 'resource', 'hidden') and self.puzzle_dir is None: raise KeyError("Cannot set a puzzle file for single file puzzles.") if key == 'answer': @@ -227,15 +250,15 @@ class Puzzle: self._dict['answer'].append(value) elif key == 'file': # Handle adding files to the puzzle - path = os.path.join(self._puzzle_dir, 'files', value) + path = os.path.join(self.puzzle_dir, 'files', value) self._dict['files'][value] = self._puzzle_file(path, value) elif key == 'resource': # Handle adding category files to the puzzle - path = os.path.join(self._puzzle_dir, '../res', value) + path = os.path.join(self.puzzle_dir, '../res', value) self._dict['files'].append(self._puzzle_file(path, value)) elif key == 'hidden': # Handle adding secret, 'hidden' files to the puzzle. - path = os.path.join(self._puzzle_dir, 'files', value) + path = os.path.join(self.puzzle_dir, 'files', value) name = self.random_hash() self._dict['files'].append(self._puzzle_file(path, name, visible=False)) elif key in self.SINGULAR_KEYS: